Menu screens: rock background, centred frame, logo, 3D hero

- Spielen hero panel shows the selected hero in 3D (cUI_NetGranny,
  game/ui/granny.h).
- All menu screens draw on the options' rock wall; the main menu's 1024
  frame is drawn centred over it, mouse and cursor shifted to match.
- The main menu's and options' own pictures and the main menu's 3D scene
  are left out (textures hidden by surface, scene call redirected).
- Logo top left: FX_SACREDLOGO.TGA, drawn added.
- Game textures by name (game/ui/texture.h), textured and added quads.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-21 00:05:55 +02:00
1 parent 34a119433a
commit db87e276bf
16 files changed
+620 -69

No files matched your search

+11 -3
View File
@@ -189,8 +189,16 @@ below; our own background). Built on the shared screen kit
Its entries are only needed for the load.
- Left: the characters, newest save first (name, class, level; "im Spiel" for
the one being played). Click selects, double click plays.
- Middle: name, class and level, the world played last with its date. (A
render of the hero is planned.)
- Middle: name, class and level, the world played last with its date, and
the hero in 3D with its equipment (2026-10-20): the game's cUI_NetGranny
(game/ui/granny.h), the control the export screen and the multiplayer
lobby showed heroes with. The character's hero file goes to
`_workpreview.pax`, LoadHeroFile (path, 0, info) reads its hero info
(class, equipment looks) without touching the player hero. Drawn without
its name label, with the white light set, size factor 3.8 (game 2.5); its
x is moved by (1365 - 1024) / 2, as it centres the model as in a 1024
layout. Migrated characters without the shared hero show none until
played.
- Right: a grid campaigns x difficulties. A cell shows the save's date,
"+ Autosave" when the autosave is newer, "Story abgeschlossen", "Neu" for a
difficulty that is open but has no world, "–" when closed. Open: Bronze and
@@ -306,5 +314,5 @@ kept, the newest file overall is the shared hero.
## Open
- Quickload (row 0) does not know characters (not needed, user 2026-10-20).
- Spielen screen: a render of the hero; scrolling the list beyond 9 characters.
- Spielen screen: scrolling the list beyond 9 characters.
- Multiplayer (see `todo.md`).
+43 -5
View File
@@ -2,8 +2,12 @@
Request (2026-10-20): the game's menu screens are laid out for 1024x768; we
design our own UIs for them. There is no 16:9 artwork for the game, so the
background is our own (option 3 of the ones discussed: dark, no picture), and
the approach is shared by all menu screens. Target: 16:9, 1920x1080, which is
background is our own, and the approach is shared by all menu screens. The
background (user, 2026-10-20: "textured dark", the flat gradient looked
black): the options window's rock wall, `GUI_OPTION_BACKGROUND<row><column>.TGA`
(4 x 3 tiles of 256), scaled to cover the render target, tinted down,
shaded towards the bottom and the sides. Game textures by name:
`game/ui/texture.h`, drawn with `game::DrawTextured` (game/ui/draw.h). Target: 16:9, 1920x1080, which is
1365x768 in UI units; wider screens only get more background at the sides
(not designed for ultrawide).
@@ -16,9 +20,7 @@ the approach is shared by all menu screens. Target: 16:9, 1920x1080, which is
layer's offset machinery with `Anchor::Canvas`; drawing, the cursor and the
window's hit tests shifted alike). Mouse events go through `screen::Handle`
(`hud::HandleCanvas`). Keys stay the window's.
- In the main menu the game renders a classic 1024x768 frame (presented 4:3).
While a screen's window is shown, `widescreen::SetWideMenuScreen` makes the
menu thread render and present the full width.
- The main menu thread renders the full width (no 4:3 frame any more).
- Parts: `screen::Panel` (dark backdrop and the game's frame, `cUI_Frame`,
game/ui/frame.h, made anew when its rect changes), `screen::Button` (a
button of the game's with the look of one of the window's,
@@ -33,6 +35,42 @@ the approach is shared by all menu screens. Target: 16:9, 1920x1080, which is
- Inside the canvas scope the HUD layer does not stretch "full 1024 layout"
quads (`g_canvasScope`).
## Main menu frame (2026-10-20)
Request: the background on all screens, the main menu's and the options
window's own pictures removed.
- cUI_Manager's UI render (`0x7587b0`, hooked in mods/screen.cpp) on the
menu thread: the background first, then `hud::RenderMenuFrame` runs the
render with drawing shifted by (render width - 1024) / 2 and a viewport on
the frame (3D parts). Windows inside draw in frame coordinates (their own
HUD scopes are nested, so they do nothing).
- Mouse positions are given to the game in frame coordinates: the
borderless mod's `ToGame` subtracts `hud::MenuFrameOffsetX`. The cursor
sprite (`cMouseCursor::DrawItem` 0x6555e0, drawn after the UI) is shifted
by drawing only.
- While a 16:9 screen is shown the frame is not shifted (offset 0): the
screen draws itself across the width.
- Full-screen windows in a game (options, `hud::RenderFramed`) get the
background beside the frame instead of black.
- The windows' pictures are left out by texture: the HUD layer tracks the
texture bound to stage 0 (`SetTexture`, device vtable 35) and skips quads
drawn with a hidden one (`hud::SetHiddenTextures`). Logged on the menu
thread (texture ids requested from the cache, by caller; names by trying
the names in the texture pak): main menu GUI_UW_OPTION1..12 + UI_MAIN_SKY,
options UW_OPTION_1_01..12. This install uses the Underworld versions; the
Ancaria ones (GUI_OPTION1..12, ...) were not seen and are not listed.
- The main menu's 3D scene (sky UI_UW_MAIN_CENTER, water with the SACRED
logo UI_MAIN_REF; render 0x75e620 on the scene object 0x75df10) is called
from cUI_MainMenu::Render (0x7122c0) at 0x712bd9; that call is redirected
to an empty function. cUI_Character (0x70f41b) still draws it.
- Logo top left (2026-10-21), 48 from the edges, 124 high: FX_SACREDLOGO.TGA,
the emblem (horns around a ring of runes) glowing blue on black without
alpha, drawn added (blend ONE/ONE, `game::DrawTexturedAdded`) as the
game's effects draw it. Other candidates: GUI_MAIN_LOGO (plain golden
lettering), SACREDSYMBOL01/02 (model atlases), the emblem on the loading
pictures (PAK\loading*.bmp, not in the paks). Screens may overlap it.
## Screens
- Spielen (the load window, cUI_Savegame): docs/characters.md, "Spielen
+11 -3
View File
@@ -211,7 +211,15 @@ space far better. No 16:9 artwork exists for the game (user, 2026-10-20):
we design our own UIs, using the width beyond the 1024 frame (now drawn in
a centred 1024x768 viewport with black sides, hud.cpp FrameScope). Design
for 16:9 (1920x1080, 1365x768 UI units), not ultrawide (user). Screen done
2026-10-20 (docs/characters.md, "Spielen screen"; docs/screens.md). Open:
the render of the selected hero (the hero panel shows text only; reuse
cUI_Character's 3D hero?), list scrolling beyond 9 characters, larger
2026-10-20 (docs/characters.md, "Spielen screen"; docs/screens.md). Hero
in 3D done 2026-10-20. Open: list scrolling beyond 9 characters, larger
heading font.
## Fine-tuning the Spielen and options screens (2026-10-21)
The user is very satisfied with the menu rework for now (rock background on
all menu screens, centred menu frame, logo, Spielen screen with the 3D
hero), but both the Spielen screen and the options window still need a lot
of fine-tuning. Details to be collected when we get to it; known so far:
list scrolling beyond 9 characters, larger heading font, the options
window's layout in the width beyond its 1024 frame.
+4 -2
View File
@@ -67,9 +67,11 @@ Elsewhere (unknown / UI, leave alone for now): `0x40694a`, `0x4ec036`,
| `0x006469d0` | loading screen: GDI BitBlt of bitmap + progress bar + text into the back buffer (fixed 1024x768) | render thread |
| `0x006a0c60` | video/intro playback (unexamined) | |
The widescreen mod gives these frames a 1024x768 D3D viewport (set at
The widescreen mod gives the loading screen a 1024x768 D3D viewport (set at
`BeginScene` when the scene depth `+0x1d8` is 0) and the borderless mod
presents only that area, scaled to the monitor.
presents only that area, scaled to the monitor. The main menu thread renders
the full width since 2026-10-20: its 1024x768 frame is drawn centred on the
menu screens' background (docs/screens.md, "Main menu frame").
## Direct3D 7 render target limit
+45
View File
@@ -10,6 +10,12 @@ namespace game {
constexpr int kDeviceDrawPrimitive = 25; // IDirect3DDevice7 vtable slots
constexpr int kDeviceSetTexture = 35;
constexpr int kDeviceSetRenderState = 20;
constexpr int kDeviceGetRenderState = 21;
constexpr DWORD kStateSourceBlend = 19; // D3DRENDERSTATETYPE
constexpr DWORD kStateDestinationBlend = 20;
constexpr DWORD kStateAlphaBlendEnable = 27;
constexpr DWORD kBlendOne = 2; // D3DBLEND
constexpr DWORD kTriangleStrip = 5;
constexpr DWORD kFvfScreenColourTexture = 0x1c4; // XYZRHW | DIFFUSE | SPECULAR | TEX1
@@ -19,6 +25,45 @@ struct ScreenVertex {
float u, v;
};
// Draws (x0, y0)-(x1, y1) with `surface` (a texture's Direct3D surface,
// game/ui/texture.h) from (u0, v0) to (u1, v1), tinted by `colour`.
inline void DrawTextured(void* device, void* surface, float x0, float y0, float x1, float y1, float u0, float v0,
float u1, float v1, uint32_t colour = 0xffffffff) {
void** vtable = *static_cast<void***>(device);
using SetTextureFn = HRESULT(__stdcall*)(void*, DWORD, void*);
using DrawPrimitiveFn = HRESULT(__stdcall*)(void*, DWORD, DWORD, void*, DWORD, DWORD);
reinterpret_cast<SetTextureFn>(vtable[kDeviceSetTexture])(device, 0, surface);
ScreenVertex quad[4] = {{x0, y0, 0, 1, colour, 0, u0, v0},
{x1, y0, 0, 1, colour, 0, u1, v0},
{x0, y1, 0, 1, colour, 0, u0, v1},
{x1, y1, 0, 1, colour, 0, u1, v1}};
reinterpret_cast<DrawPrimitiveFn>(vtable[kDeviceDrawPrimitive])(device, kTriangleStrip, kFvfScreenColourTexture,
quad, 4, 0);
reinterpret_cast<SetTextureFn>(vtable[kDeviceSetTexture])(device, 0, nullptr);
}
// DrawTextured added onto what is drawn (source and destination blend ONE):
// for the effect textures, drawn on black without alpha.
inline void DrawTexturedAdded(void* device, void* surface, float x0, float y0, float x1, float y1, float u0,
float v0, float u1, float v1) {
void** vtable = *static_cast<void***>(device);
using SetRenderStateFn = HRESULT(__stdcall*)(void*, DWORD, DWORD);
using GetRenderStateFn = HRESULT(__stdcall*)(void*, DWORD, DWORD*);
const auto set = reinterpret_cast<SetRenderStateFn>(vtable[kDeviceSetRenderState]);
const auto get = reinterpret_cast<GetRenderStateFn>(vtable[kDeviceGetRenderState]);
DWORD blending = 0, source = 0, destination = 0;
get(device, kStateAlphaBlendEnable, &blending);
get(device, kStateSourceBlend, &source);
get(device, kStateDestinationBlend, &destination);
set(device, kStateAlphaBlendEnable, TRUE);
set(device, kStateSourceBlend, kBlendOne);
set(device, kStateDestinationBlend, kBlendOne);
DrawTextured(device, surface, x0, y0, x1, y1, u0, v0, u1, v1);
set(device, kStateAlphaBlendEnable, blending);
set(device, kStateSourceBlend, source);
set(device, kStateDestinationBlend, destination);
}
// Fills (x0, y0)-(x1, y1), ARGB colours per corner: top left, top right,
// bottom left, bottom right (blended between them).
inline void FillCorners(void* device, float x0, float y0, float x1, float y1, uint32_t topLeft, uint32_t topRight,
+67
View File
@@ -0,0 +1,67 @@
#pragma once
// cUI_NetGranny (vtable 0x894c8c, 0x634 bytes, window name UI_WND_GRANNY): the
// game's 3D view of a hero with its equipment, as the export screen's slots
// (cUI_Character +0x1b4, 0x710f31) and the multiplayer lobby showed them.
//
// The model comes from a hero info (0x22c bytes: class at +4, the equipment's
// looks), which LoadHeroFile (0x61d190, cdecl (path, 0, info)) reads from a
// hero file without touching the player hero (0x70bf2c).
//
// Render (vf14 -> vf44 0x6f17f0 (device, 0, 1), only while visible; the last
// argument adds the name and level below the model, 0x6f1260). With +0x53d
// clear the model stands at the rect's horizontal centre, its feet at
// rect.y + h * k, projected with the creatures' ortho camera (x -512..512
// around the screen centre, widened by the widescreen mod); size factor
// +0x630 (2.5 from the constructor). Two lights are made on the first render
// (0x6f0f10): a dim blue set (the lobby) with +0x53f clear, a white one with
// it set. With +0x53d set (cUI_Character) the model is placed in a
// perspective scene instead.
#include <cstddef>
#include <cstdint>
#include "game/ui/tabs.h"
#include "game/ui/text.h"
namespace game {
constexpr size_t kNetGrannySize = 0x634;
constexpr size_t kNetGrannyInScene = 0x53d; // u8, see above
constexpr size_t kNetGrannyWhiteLight = 0x53f; // u8, before the first render
constexpr size_t kNetGrannyScale = 0x630; // float
constexpr size_t kHeroInfoSize = 0x22c;
// cUI_NetGranny::cUI_NetGranny (0x6f06f0, thiscall (rect by value), ret 0xc).
inline uint8_t* NewNetGranny(UiRect rect) {
void* memory = GameNew(kNetGrannySize);
using ConstructFn = uint8_t*(__fastcall*)(void*, void*, UiRect);
return memory ? reinterpret_cast<ConstructFn>(0x006f06f0)(memory, nullptr, rect) : nullptr;
}
// The model alone, without its name and level (vf44, thiscall (device,
// 0, label), ret 0xc).
inline void RenderNetGranny(uint8_t* granny, void* device) {
using RenderFn = void(__fastcall*)(void*, void*, void*, uint32_t, uint8_t);
reinterpret_cast<RenderFn>((*reinterpret_cast<void***>(granny))[0x44 / 4])(granny, nullptr, device, 0, 0);
}
// Shows the hero of `info` (0x6f4b20, thiscall (info, x, y, z, turn), ret
// 0x14; the four floats place it in the perspective scene only).
inline void SetNetGrannyHero(uint8_t* granny, const void* info) {
using SetFn = void(__fastcall*)(void*, void*, const void*, float, float, float, float);
reinterpret_cast<SetFn>(0x006f4b20)(granny, nullptr, info, 0.0f, 0.0f, 0.0f, 0.0f);
}
// No hero (0x6f4ae0, thiscall ()).
inline void ClearNetGrannyHero(uint8_t* granny) {
reinterpret_cast<void(__fastcall*)(void*, void*)>(0x006f4ae0)(granny, nullptr);
}
// The hero info of a hero file (a path as the game's file functions take it);
// false if it cannot be read. `info` holds kHeroInfoSize bytes.
inline bool ReadHeroInfo(const char* path, void* info) {
using LoadFn = uint8_t(__cdecl*)(const char*, uint32_t, void*);
return reinterpret_cast<LoadFn>(0x0061d190)(path, 0, info) != 0;
}
} // namespace game
+34
View File
@@ -0,0 +1,34 @@
#pragma once
// The game's textures by name (a .TGA in the texture paks), as cGranny's
// effects load them (0x401232): the texture manager (0x656c80, cdecl (),
// created on first use, kept at 0x13e57b4) gives the id of a name (0x657620,
// thiscall (name), ret 4); the texture cache (0x13e57b8) loads it on demand
// (0x65f010, thiscall (id, 0), ret 8), its Direct3D surface at +0x14 (as
// cUI_Button's render binds it, 0x72afb7).
#include <cstddef>
#include <cstdint>
namespace game {
constexpr uintptr_t kTextureCache = 0x013e57b8;
constexpr size_t kTextureSurface = 0x14;
inline uint32_t TextureId(const char* name) {
void* manager = reinterpret_cast<void*(__cdecl*)()>(0x00656c80)();
using IdFn = uint32_t(__fastcall*)(void*, void*, const char*);
return manager ? reinterpret_cast<IdFn>(0x00657620)(manager, nullptr, name) : 0;
}
// The Direct3D surface of texture `id`, loaded if needed; null if none.
inline void* TextureSurface(uint32_t id) {
void* cache = *reinterpret_cast<void**>(kTextureCache);
if (!cache || !id) {
return nullptr;
}
using GetFn = uint8_t*(__fastcall*)(void*, void*, uint32_t, uint32_t);
uint8_t* texture = reinterpret_cast<GetFn>(0x0065f010)(cache, nullptr, id, 0);
return texture ? *reinterpret_cast<void**>(texture + kTextureSurface) : nullptr;
}
} // namespace game
+1
View File
@@ -138,6 +138,7 @@ POINT ToGame(POINT p) {
POINT result;
result.x = std::clamp(MulDiv(p.x - layout.client.left, layout.gameWidth, width), 0, layout.gameWidth - 1);
result.y = std::clamp(MulDiv(p.y - layout.client.top, layout.gameHeight, height), 0, layout.gameHeight - 1);
result.x -= mods::hud::MenuFrameOffsetX(); // the main menu's centred frame
return result;
}
+70 -3
View File
@@ -49,6 +49,7 @@
#include "game/ui/button.h"
#include "game/ui/control.h"
#include "game/ui/events.h"
#include "game/ui/granny.h"
#include "game/ui/text.h"
#include "game/ui/texts.h"
#include "game/ui/windows.h"
@@ -386,6 +387,8 @@ std::atomic<bool> g_autosavePending{false};
bool g_inGame = false; // a game runs (the load window opens from the Esc menu too)
std::atomic<uint64_t> g_lastSaveAt{0}; // GetTickCount64 of the last save of any kind
void ForgetHeroModel(); // the files may have changed: read the hero anew
std::string Description(const json& meta) {
return Convert(FromUtf8(meta.value("name", std::string())) + L" - " + ClassName(meta.value("class", 0u)),
CP_ACP);
@@ -417,6 +420,7 @@ void RebuildList() {
DeleteFileA((work + file).c_str());
}
g_rows.clear();
ForgetHeroModel();
for (const std::string& name : FilesIn(CharactersDir(), "*.sacredK")) {
if (EndsWith(name, kAutosaveExtension) || g_rows.size() >= kMaxRows) {
continue;
@@ -616,8 +620,10 @@ std::atomic<int> g_loadRow{-1};
// Layout on the canvas (1365x768): three panels side by side (sizes in the
// frames' 64 steps, screen::kFrameStep), centred, a row of buttons below.
constexpr int kPanelTop = 96; // the title above the frames' ornament
constexpr int kPanelHeight = 8 * screen::kFrameStep;
// Top: room for the heading and the frames' ornament; the buttons end
// above the screen's bottom.
constexpr int kPanelTop = 164 + screen::kFrameOrnament;
constexpr int kPanelHeight = 7 * screen::kFrameStep;
constexpr int kListWidth = 5 * screen::kFrameStep;
constexpr int kHeroWidth = 6 * screen::kFrameStep;
constexpr int kWorldsWidth = 6 * screen::kFrameStep;
@@ -1329,6 +1335,64 @@ void DrawList(void* device) {
}
}
// The selected character's hero in 3D (game/ui/granny.h): its hero file is
// unpacked to the game's save folder (_work) and read as a hero info. A
// migrated character without the shared hero has none until it is played.
constexpr float kHeroModelScale = 3.8f; // the game's 2.5 fills a third of the panel
constexpr int kHeroModelFeetAboveBottom = 24;
constexpr char kWorkPreviewHero[] = "preview.pax";
constexpr char kGamePreviewHero[] = "Save\\preview.pax";
struct HeroModel {
uint8_t* control = nullptr;
std::string file; // whose hero it shows
bool hasHero = false;
} g_heroModel;
void ForgetHeroModel() {
g_heroModel.file.clear();
}
void ShowHeroOf(const Row& row) {
if (row.file == g_heroModel.file) {
return;
}
g_heroModel.file = row.file;
g_heroModel.hasHero = false;
CharacterFile character(row.file);
std::optional<Bytes> hero = character.Open() ? character.Read("hero") : std::nullopt;
std::vector<uint8_t> info(game::kHeroInfoSize);
if (hero && WriteFileReplacing(WorkDir() + kWorkPreviewHero, *hero) &&
game::ReadHeroInfo(kGamePreviewHero, info.data())) {
game::SetNetGrannyHero(g_heroModel.control, info.data());
g_heroModel.hasHero = true;
} else {
game::ClearNetGrannyHero(g_heroModel.control);
}
DeleteFileA((WorkDir() + kWorkPreviewHero).c_str());
}
// The model stands centred in `rect` (canvas coordinates). The control centres
// it as in a 1024 wide layout (x - 512 around the screen centre); the canvas
// is centred too, so its x is moved by the difference.
void DrawHeroModel(void* device, const Row& row, game::UiRect rect) {
rect.x -= (screen::kWidth - 1024) / 2;
if (!g_heroModel.control) {
g_heroModel.control = game::NewNetGranny(rect);
if (!g_heroModel.control) {
return;
}
g_heroModel.control[game::kNetGrannyWhiteLight] = 1;
*reinterpret_cast<float*>(g_heroModel.control + game::kNetGrannyScale) = kHeroModelScale;
game::SetControlShown(g_heroModel.control, true);
}
*reinterpret_cast<game::UiRect*>(g_heroModel.control + game::kControlRect) = rect;
ShowHeroOf(row);
if (g_heroModel.hasHero) {
game::RenderNetGranny(g_heroModel.control, device);
}
}
void DrawHero(void* device) {
const game::UiRect content = g_heroPanel.Draw(device, kHeroRect);
const Row* row = SelectedCharacter();
@@ -1348,6 +1412,9 @@ void DrawHero(void* device) {
line(CampaignName(last->campaign) + L" · " + DifficultyName(last->difficulty), screen::kTextColour);
line(FormatTime(last->savedAt), screen::kDimColour);
}
// The control puts the feet at 5/6 of its rect's height (0x88f04c).
const int feet = content.y + content.h - kHeroModelFeetAboveBottom;
DrawHeroModel(device, *row, {content.x, y, content.w, static_cast<int16_t>((feet - y) * 6 / 5)});
}
void DrawCell(void* device, const Row& row, int campaign, int difficulty) {
@@ -1375,7 +1442,7 @@ void DrawCell(void* device, const Row& row, int campaign, int difficulty) {
if (cell.autosave && cell.world) {
screen::Text(device, bottom, L"+ Autosave", screen::kHeadingColour, game::kTextCenter);
} else if (row.storyCompleted[difficulty]) {
screen::Text(device, bottom, L"Story abgeschlossen", screen::kDimColour, game::kTextCenter);
screen::Text(device, bottom, L"abgeschlossen", screen::kDimColour, game::kTextCenter);
}
}
+111 -13
View File
@@ -21,6 +21,7 @@
// Enabled=1
#include <algorithm>
#include <atomic>
#include <cstdint>
#include <cstring>
#include <vector>
@@ -31,6 +32,7 @@
#include "game/render/dx_driver7.h"
#include "game/ui/events.h"
#include "mods/hud.h"
#include "mods/screen.h"
#include "mods/widescreen.h"
#include "re/config.h"
#include "re/hook.h"
@@ -268,6 +270,29 @@ private:
CursorOffset cursor_;
};
// RAII: shift drawing only (the cursor is not seen shifted), for things
// already in the shifted layout's coordinates (the main menu's cursor).
class DrawShift {
public:
explicit DrawShift(int dx) : active_(!g_drawScoped && dx != 0) {
if (active_) {
g_drawScoped = true;
g_offsetX = dx;
}
}
~DrawShift() {
if (active_) {
g_drawScoped = false;
g_offsetX = 0;
}
}
DrawShift(const DrawShift&) = delete;
DrawShift& operator=(const DrawShift&) = delete;
private:
bool active_;
};
// Stores the offsets of a mouse event's x fields (game/ui/events.h); returns
// their count (0: no mouse event).
int MouseEventXOffsets(void* event, int (&offsets)[2]) {
@@ -383,13 +408,47 @@ using DrawIndexedPrimitiveFn = HRESULT(__stdcall*)(void* device, DWORD type, DWO
DrawPrimitiveFn g_drawPrimitive = nullptr;
DrawIndexedPrimitiveFn g_drawIndexedPrimitive = nullptr;
// Textures not drawn (SetHiddenTextures): the surface bound to stage 0 is
// tracked per thread (each thread draws with its own device calls).
constexpr int kHiddenTextureMax = 64;
std::atomic<void*> g_hiddenTextures[kHiddenTextureMax] = {};
thread_local void* g_boundTexture = nullptr;
using SetTextureFn = HRESULT(__stdcall*)(void* device, DWORD stage, void* surface);
SetTextureFn g_setTexture = nullptr;
HRESULT __stdcall SetTextureTracked(void* device, DWORD stage, void* surface) {
if (stage == 0) {
g_boundTexture = surface;
}
return g_setTexture(device, stage, surface);
}
bool BoundTextureHidden() {
if (!g_boundTexture) {
return false;
}
for (const auto& hidden : g_hiddenTextures) {
if (hidden.load(std::memory_order_relaxed) == g_boundTexture) {
return true;
}
}
return false;
}
HRESULT __stdcall DrawPrimitiveShifted(void* device, DWORD type, DWORD fvf, void* vertices, DWORD count,
DWORD flags) {
if (BoundTextureHidden()) {
return S_OK;
}
return g_drawPrimitive(device, type, fvf, ShiftVertices(fvf, vertices, count), count, flags);
}
HRESULT __stdcall DrawIndexedPrimitiveShifted(void* device, DWORD type, DWORD fvf, void* vertices, DWORD count,
WORD* indices, DWORD indexCount, DWORD flags) {
if (BoundTextureHidden()) {
return S_OK;
}
return g_drawIndexedPrimitive(device, type, fvf, ShiftVertices(fvf, vertices, count), count, indices,
indexCount, flags);
}
@@ -440,30 +499,25 @@ HRESULT __stdcall ClearShifted(void* device, DWORD count, Rect7* rects, DWORD fl
using GetViewportFn = HRESULT(__stdcall*)(void* device, Viewport7* viewport);
constexpr int kVtableGetViewport = 15; // IDirect3DDevice7 (13 SetViewport, 14 MultiplyTransform)
constexpr DWORD kClearTarget = 1; // D3DCLEAR_TARGET
// RAII for full-screen windows (character export, save games) that fill the
// 1024 frame with a picture: the render target left and right of the frame is
// cleared black, and the window draws with a viewport on the frame. The
// RAII for full-screen windows (options in a game) that fill the 1024 frame
// with a picture: the menu screens' background (mods/screen.h) goes under
// it, and the window draws with a viewport on the frame. The
// export screen's 3D scene (heroes, background) uses a projection for a
// 1024x768 viewport; across the widened one it was stretched 1.8x wide, away
// from the slot labels and hit areas (measured on a screenshot, 2026-10-18).
// Clear clips to the viewport, so the sides are cleared before it is set.
class FrameScope {
public:
FrameScope(void* window, void* device, int dx) {
const auto* w = static_cast<const uint8_t*>(window);
if (dx == 0 || g_drawScoped || !device || !g_setViewport || !g_clear || (w[kControlFlags] & 1) == 0) {
if (dx == 0 || g_drawScoped || !device || !g_setViewport || (w[kControlFlags] & 1) == 0) {
return;
}
auto getViewport = reinterpret_cast<GetViewportFn>((*static_cast<void***>(device))[kVtableGetViewport]);
if (FAILED(getViewport(device, &previous_))) {
return;
}
const LONG height = static_cast<LONG>(previous_.height);
Rect7 sides[] = {{0, 0, dx, height},
{dx + kLayoutWidth, 0, mods::widescreen::RenderWidth(), height}};
g_clear(device, 2, sides, kClearTarget, 0, 1.0f, 0);
mods::screen::DrawBackground(device);
Viewport7 frame = {static_cast<DWORD>(dx), 0, kLayoutWidth, 768, previous_.minZ, previous_.maxZ};
device_ = device;
g_setViewport(device, &frame);
@@ -512,6 +566,10 @@ void OnDevice(void* device) {
g_drawIndexedPrimitive = reinterpret_cast<DrawIndexedPrimitiveFn>(vtable[26]);
re::AttachRuntimeHook("D3D7 DrawIndexedPrimitive", reinterpret_cast<void**>(&g_drawIndexedPrimitive),
reinterpret_cast<void*>(&DrawIndexedPrimitiveShifted));
// SetTexture 35.
g_setTexture = reinterpret_cast<SetTextureFn>(vtable[35]);
re::AttachRuntimeHook("D3D7 SetTexture", reinterpret_cast<void**>(&g_setTexture),
reinterpret_cast<void*>(&SetTextureTracked));
// Clear 10, SetViewport 13.
g_clear = reinterpret_cast<ClearFn>(vtable[10]);
re::AttachRuntimeHook("D3D7 Clear", reinterpret_cast<void**>(&g_clear), reinterpret_cast<void*>(&ClearShifted));
@@ -796,6 +854,41 @@ int HandleCentred(void* event, int (*body)(void* context), void* context) {
return body(context);
}
// Offset of the main menu's 1024 frame while it is drawn centred (set each
// menu frame by RenderMenuFrame; 0 while a 16:9 screen draws itself).
std::atomic<int> g_menuFrameDx = 0;
void SetHiddenTextures(void* const* surfaces, int count) {
for (int i = 0; i < kHiddenTextureMax; ++i) {
g_hiddenTextures[i].store(i < count ? surfaces[i] : nullptr, std::memory_order_relaxed);
}
}
int MenuFrameOffsetX() {
return mods::widescreen::MenuIsShowing() ? g_menuFrameDx.load() : 0;
}
int RenderMenuFrame(void* device, bool framed, int (*body)(void* context), void* context) {
const int width = mods::widescreen::RenderWidth();
const int dx = framed && width > kLayoutWidth ? (width - kLayoutWidth) / 2 : 0;
g_menuFrameDx = dx;
Viewport7 previous;
auto getViewport =
device ? reinterpret_cast<GetViewportFn>((*static_cast<void***>(device))[kVtableGetViewport]) : nullptr;
if (dx == 0 || g_drawScoped || !g_setViewport || !getViewport || FAILED(getViewport(device, &previous))) {
return body(context);
}
Viewport7 frame = {static_cast<DWORD>(dx), 0, kLayoutWidth, 768, previous.minZ, previous.maxZ};
g_setViewport(device, &frame);
g_drawScoped = true; // the windows inside draw in the frame's coordinates
g_offsetX = dx;
const int result = body(context);
g_offsetX = 0;
g_drawScoped = false;
g_setViewport(device, &previous);
return result;
}
int CanvasOffsetX() {
return OffsetFor(Anchor::Canvas);
}
@@ -830,12 +923,17 @@ int __fastcall UiTaskbar_HitTest(void* self, void* edx, int x, int y) {
return UiTaskbar_HitTest_original(self, edx, mods::hud::HitTestX(self, x), y);
}
// The item carried by the mouse cursor (0x6555e0, drawn by the render loop
// after the UI) is placed like UI items: relative to x = 512 of the layout.
// Items are only picked up from centred windows.
// The cursor and the item it carries (0x6555e0, drawn by the render loop
// after the UI) are placed like UI items: relative to x = 512 of the layout.
// Items are only picked up from centred windows. In the main menu the cursor
// is in the centred frame's coordinates (MenuFrameOffsetX).
int __fastcall MouseCursor_DrawItem(void* self, void* edx, int a, int b);
RE_HOOK(0x006555e0, MouseCursor_DrawItem);
int __fastcall MouseCursor_DrawItem(void* self, void* edx, int a, int b) {
if (mods::widescreen::MenuIsShowing()) {
mods::hud::DrawShift shift(mods::hud::MenuFrameOffsetX());
return MouseCursor_DrawItem_original(self, edx, a, b);
}
mods::hud::DrawOffset offset(nullptr, mods::hud::OffsetFor(Anchor::Center));
return MouseCursor_DrawItem_original(mods::hud::CursorFor(self), edx, a, b);
}
+14 -2
View File
@@ -26,8 +26,8 @@ int VisibleDialogTooltips(void** out, int max);
// shifted) or event handler (mouse position shifted by the window's offset).
int RenderCentred(void* window, int (*body)(void* context), void* context);
int HandleCentred(void* event, int (*body)(void* context), void* context);
// RenderCentred for full-screen windows: black beside the 1024 frame, drawn
// with a viewport on it (as the save game and export screens).
// RenderCentred for full-screen windows: the menu screens' background beside
// the 1024 frame, drawn with a viewport on it (as the options in a game).
int RenderFramed(void* window, void* device, int (*body)(void* context), void* context);
// 16:9 screens (mods/screen.h): laid out on a 1365x768 canvas, centred in
@@ -40,4 +40,16 @@ int CanvasOffsetX();
int RenderCanvas(void* window, int (*body)(void* context), void* context);
int HandleCanvas(void* event, int (*body)(void* context), void* context);
// The main menu (mods/screen.cpp): with `framed`, body(context) draws the
// menu's 1024x768 frame centred in the render width (shifted, with a
// viewport on it); mouse positions are then given in frame coordinates
// (MenuFrameOffsetX, applied by the borderless mod). Without, the menu draws
// unshifted (a 16:9 screen is shown).
int RenderMenuFrame(void* device, bool framed, int (*body)(void* context), void* context);
int MenuFrameOffsetX();
// Quads drawn with one of these textures (Direct3D surfaces) are left out,
// on every thread, until the list changes (at most 64).
void SetHiddenTextures(void* const* surfaces, int count);
} // namespace mods::hud
+156 -11
View File
@@ -6,25 +6,38 @@
#include <atomic>
#include <mutex>
#include <set>
#include <vector>
#include <windows.h>
#include "game/addresses.h"
#include "game/render/dx_driver7.h"
#include "game/ui/button.h"
#include "game/ui/control.h"
#include "game/ui/draw.h"
#include "game/ui/events.h"
#include "game/ui/frame.h"
#include "game/ui/texture.h"
#include "game/ui/texts.h"
#include "mods/widescreen.h"
#include "re/hook.h"
#include "re/patch.h"
namespace mods::screen {
namespace {
// Background (decided 2026-10-20: our own, no picture): dark, a little warm
// at the top, darker towards the bottom and the sides.
constexpr uint32_t kBackgroundTop = 0xff1c1712;
constexpr uint32_t kBackgroundBottom = 0xff070605;
constexpr uint32_t kVignette = 0xa0000000;
// Background (user, 2026-10-20: "textured dark", then "doesn't need to be that
// dark, but apply it to all screens"): the options window's rock wall,
// GUI_OPTION_BACKGROUND<row><column>.TGA (4 x 3 tiles of 256, a 1024x768
// picture), scaled to cover the render target, a little darkened, darker
// towards the bottom and the sides.
constexpr int kBackgroundColumns = 4;
constexpr int kBackgroundRows = 3;
constexpr float kBackgroundTile = 256.0f;
constexpr uint32_t kBackgroundTint = 0xffa0a0a0;
constexpr uint32_t kShadeTop = 0x10000000;
constexpr uint32_t kShadeBottom = 0x70000000;
constexpr uint32_t kVignette = 0x70000000;
constexpr int kVignetteWidth = 240;
constexpr uint32_t kPanelBackdrop = 0xb4000000;
constexpr int kHeadingHeight = 30;
@@ -33,8 +46,8 @@ constexpr int kHeadingHeight = 30;
// frames).
std::atomic<unsigned> g_frame{1};
// The windows whose screen is shown: the menu thread renders the full width
// while any is.
// The windows whose screen is shown: the main menu's frame is not drawn
// centred while any is (the screen draws itself across the width).
std::mutex g_shownLock;
std::set<const void*> g_shown;
@@ -45,19 +58,98 @@ void NoteShown(const void* window, bool shown) {
} else {
g_shown.erase(window);
}
widescreen::SetWideMenuScreen(!g_shown.empty());
}
// The windows' own pictures, replaced by the background (user, 2026-10-20:
// "the previous backgrounds from settings and mainMenu can be removed"), 12
// tiles each (logged: the textures the menu thread asks for, by name): the
// main menu GUI_UW_OPTION<1..12>.TGA with the sky above it UI_MAIN_SKY.TGA,
// the options window UW_OPTION_1_<01..12>.TGA. The main menu's 3D scene
// behind its picture is not drawn either (game::RemoveMenuScene).
std::vector<uint32_t> PictureTextureIds() {
std::vector<uint32_t> ids = {game::TextureId("UI_MAIN_SKY.TGA")};
char name[40];
for (int i = 1; i <= 12; ++i) {
wsprintfA(name, "GUI_UW_OPTION%d.TGA", i);
ids.push_back(game::TextureId(name));
wsprintfA(name, "UW_OPTION_1_%02d.TGA", i);
ids.push_back(game::TextureId(name));
}
return ids;
}
// Their surfaces (loaded now, as the window is about to draw them; the cache
// may have made them anew) go to the HUD layer's hidden list.
void HidePictures() {
static const std::vector<uint32_t> ids = PictureTextureIds();
std::vector<void*> surfaces;
for (uint32_t id : ids) {
if (void* surface = game::TextureSurface(id)) {
surfaces.push_back(surface);
}
}
hud::SetHiddenTextures(surfaces.data(), static_cast<int>(surfaces.size()));
}
bool ShownAny() {
std::lock_guard guard(g_shownLock);
return !g_shown.empty();
}
// The game's logo top left (user, 2026-10-21: "the bright blueish one"),
// kLogoMargin from the edges: FX_SACREDLOGO.TGA, the emblem (horns around a
// ring of runes) glowing blue on black, no alpha, added onto the background as
// the game's effects draw it; the glow within u kLogoU0..U1 of the square
// texture, the full height (measured on a screenshot of it drawn whole).
constexpr char kLogoTexture[] = "FX_SACREDLOGO.TGA";
constexpr float kLogoU0 = 0.13f, kLogoU1 = 0.87f;
static_assert(kLogoWidth == static_cast<int>(kLogoHeight * (kLogoU1 - kLogoU0)));
void DrawLogo(void* device) {
if (void* logo = game::TextureSurface(game::TextureId(kLogoTexture))) {
const float edge = static_cast<float>(kLogoMargin);
game::DrawTexturedAdded(device, logo, edge, edge, edge + kLogoWidth, edge + kLogoHeight, kLogoU0, 0, kLogoU1,
1);
}
}
void DrawRockWall(void* device, float width, float height) {
const float scale = std::max(width / (kBackgroundColumns * kBackgroundTile),
height / (kBackgroundRows * kBackgroundTile));
const float tile = kBackgroundTile * scale;
const float left = (width - kBackgroundColumns * tile) / 2;
const float top = (height - kBackgroundRows * tile) / 2;
for (int row = 0; row < kBackgroundRows; ++row) {
for (int column = 0; column < kBackgroundColumns; ++column) {
char name[40];
wsprintfA(name, "GUI_OPTION_BACKGROUND%d%d.TGA", row, column);
if (void* surface = game::TextureSurface(game::TextureId(name))) {
const float x = left + column * tile, y = top + row * tile;
game::DrawTextured(device, surface, x, y, x + tile, y + tile, 0, 0, 1, 1, kBackgroundTint);
}
}
}
}
} // namespace
bool AnyShown() {
return ShownAny();
}
void DrawBackground(void* device) {
const float width = static_cast<float>(std::max(widescreen::RenderWidth(), kWidth));
const float height = static_cast<float>(kHeight);
game::FillVertical(device, 0, 0, width, height, kBackgroundTop, kBackgroundBottom);
HidePictures();
game::FillRect(device, 0, 0, width, height, 0xff000000);
DrawRockWall(device, width, height);
game::FillVertical(device, 0, 0, width, height, kShadeTop, kShadeBottom);
game::FillHorizontal(device, 0, 0, kVignetteWidth, height, kVignette, 0);
game::FillHorizontal(device, width - kVignetteWidth, 0, width, height, 0, kVignette);
DrawLogo(device);
}
} // namespace
int Render(void* window, void* device, int (*body)(void* context), void* context) {
const bool shown = game::ControlShown(window);
NoteShown(window, shown);
@@ -151,4 +243,57 @@ bool DoubleClick::Click(int target) {
return twice;
}
} // namespace mods::screen
// The main menu (cUI_Manager's UI render 0x7587b0, thiscall (1 argument), ret
// 4, from the menu thread loop 0x756aa1 and the in-game render 0x60ea74): on
// the menu thread the background goes first, then the menu's 1024x768 frame
// is drawn centred over it (hud::RenderMenuFrame); a 16:9 screen draws its own.
namespace game {
uint32_t __fastcall UiManager_Render(void* self, void* edx, uint32_t argument);
RE_HOOK(0x007587b0, UiManager_Render);
uint32_t __fastcall UiManager_Render(void* self, void* edx, uint32_t argument) {
using namespace mods;
if (!widescreen::IsMenuThread() || widescreen::IsClassicFrame() || widescreen::RenderWidth() == 0) {
return UiManager_Render_original(self, edx, argument);
}
auto* driver = *reinterpret_cast<DxDriver7**>(addr::g_dxDriver);
void* device = driver ? driver->device : nullptr;
const bool framed = !screen::AnyShown();
if (framed && device) {
screen::DrawBackground(device);
}
struct Call {
void* self;
void* edx;
uint32_t argument;
uint32_t result;
} call{self, edx, argument, 0};
hud::RenderMenuFrame(
device, framed,
[](void* context) {
auto* c = static_cast<Call*>(context);
c->result = UiManager_Render_original(c->self, c->edx, c->argument);
return 0;
},
&call);
return call.result;
}
// cUI_MainMenu::Render (vf14 0x7122c0) draws a 3D scene (sky UI_UW_MAIN_CENTER,
// water UI_MAIN_REF; 0x75e620, thiscall (1 argument), ret 4, on the scene
// object 0x75df10) behind its picture: the call at 0x712bd9 goes nowhere.
constexpr uint32_t kMainMenuSceneCall = 0x00712bd9;
constexpr uint32_t kRenderMenuScene = 0x0075e620;
void __fastcall SkipMenuScene(void*, void*, uint32_t) {}
void RemoveMenuScene() {
re::RedirectCall(kMainMenuSceneCall, kRenderMenuScene, reinterpret_cast<const void*>(&SkipMenuScene));
}
RE_STARTUP(MenuSceneStartup, RemoveMenuScene);
} // namespace game
+13 -2
View File
@@ -33,6 +33,12 @@ constexpr int kMargin = kFrameOrnament + 16; // canvas edge to a panel
constexpr int kPanelGap = 96; // between two panels' rects, as the options window's columns
constexpr int kPanelPadding = 16; // panel rect to its content
// The logo in the background's top left corner (render target coordinates;
// screens may overlap it).
constexpr int kLogoMargin = 48;
constexpr int kLogoHeight = 124;
constexpr int kLogoWidth = kLogoHeight * 74 / 100; // the emblem's aspect (screen.cpp)
constexpr uint16_t kFont = 1;
constexpr uint32_t kTextColour = 0xffe6d6aa; // as the options' texts
constexpr uint32_t kHeadingColour = 0xfff0c060; // as the options' headings
@@ -41,9 +47,14 @@ constexpr uint32_t kRowColour = 0x40302418;
constexpr uint32_t kRowHoverColour = 0x80403020;
constexpr uint32_t kRowSelectedColour = 0xc0604818;
// The background of the menu screens across the whole render target (also
// beside the main menu's centred frame and full-screen windows in a game).
void DrawBackground(void* device);
// Whether a screen is shown (the main menu then does not draw centred).
bool AnyShown();
// Draws the screen of `window`: the background across the whole render
// target, then body(context) in canvas coordinates. While `window` is shown
// the main menu thread renders the full width (widescreen::SetWideMenuScreen).
// target, then body(context) in canvas coordinates.
int Render(void* window, void* device, int (*body)(void* context), void* context);
// Runs body(context) with the event's mouse position in canvas coordinates.
int Handle(void* event, int (*body)(void* context), void* context);
+14 -17
View File
@@ -63,14 +63,14 @@ bool g_pushesPatched = false;
int g_renderWidth = 0;
int g_renderHeight = 0;
// Frames from the main menu thread and the loading screen are laid out for
// 1024x768 ("classic"); everything else is the widened in-game view.
// The loading screen is laid out for 1024x768 ("classic"); the main menu
// thread renders the full width too (its windows drawn centred by
// mods/screen.cpp), the in-game view is widened.
std::atomic<DWORD> g_menuThread = 0;
// Whether the most recent scene (on any thread) was a classic one: the main
// menu is showing. UI input is processed outside the menu thread.
std::atomic<bool> g_lastSceneClassic = false;
// Whether the most recent scene (on any thread) was the main menu's or the
// loading screen's. UI input is processed outside the menu thread.
std::atomic<bool> g_lastSceneMenu = false;
thread_local int g_loadingScreenDepth = 0;
std::atomic<bool> g_wideMenuScreen = false;
// D3DVIEWPORT7 (d3d.h is not in current Windows SDKs).
struct Viewport7 {
@@ -340,18 +340,15 @@ int RenderWidth() {
}
bool MenuIsShowing() {
return g_lastSceneClassic;
return g_lastSceneMenu;
}
bool IsClassicFrame() {
if (g_renderWidth == 0) {
return false;
}
return g_loadingScreenDepth > 0 || (GetCurrentThreadId() == g_menuThread.load() && !g_wideMenuScreen);
return g_renderWidth != 0 && g_loadingScreenDepth > 0;
}
void SetWideMenuScreen(bool shown) {
g_wideMenuScreen = shown;
bool IsMenuThread() {
return GetCurrentThreadId() == g_menuThread.load();
}
} // namespace mods::widescreen
@@ -376,13 +373,13 @@ void __fastcall DxDriver7_LoadingScreen(DxDriver7* self, void* edx, uint32_t pro
--g_loadingScreenDepth;
}
// Menus draw their 3D parts for a 1024x768 screen: give them a 1024x768
// viewport (the borderless mod then presents just that area, scaled), and the
// world the full render size.
// The loading screen draws for a 1024x768 screen: give it a 1024x768
// viewport (the borderless mod then presents just that area, scaled), and
// everything else the full render size.
int __fastcall DxDriver7_BeginScene(DxDriver7* self, void* edx) {
mods::hud::OnDevice(self->device);
if (self->sceneDepth == 0) {
g_lastSceneClassic = IsClassicFrame();
g_lastSceneMenu = IsClassicFrame() || IsMenuThread();
}
if (g_renderWidth != 0 && self->sceneDepth == 0 && self->device) {
const bool classic = IsClassicFrame();
+6 -8
View File
@@ -11,20 +11,18 @@ namespace mods::widescreen {
// the world view to match. No-op when disabled.
void ApplyRenderSize(game::DxDisplayMode& mode, HWND window);
// True while the current thread renders a frame laid out for 1024x768 (main
// menu, loading screen); the borderless mod then presents only that area.
// True while the current thread renders a frame laid out for 1024x768 (the
// loading screen); the borderless mod then presents only that area.
bool IsClassicFrame();
// Applied render width (0 while the original 1024x768 is in use).
int RenderWidth();
// True while the main menu / loading screen (classic layout) is on screen,
// regardless of the calling thread.
// True while the main menu or the loading screen is on screen, regardless of
// the calling thread.
bool MenuIsShowing();
// While a 16:9 menu screen (mods/screen.h) is shown from the main menu, the
// menu thread renders and presents the full render size instead of the
// classic 1024x768.
void SetWideMenuScreen(bool shown);
// True on the main menu's render thread (cUI_Manager's thread loop).
bool IsMenuThread();
} // namespace mods::widescreen
+20
View File
@@ -351,3 +351,23 @@
0x00711fa0 cUI_MainMenu::HandleEvent # vf10: sets g_campaign 0x182cdc8 by entry name (NEWGAME 0, NEWADDON 1)
0x00753d70 cUI_StaticText64FX::cUI_StaticText64FX # (name, rect, parent, ?, text id, command, flags)
0x0070e2a0 cUI_Character::Show # vf24 (shown), ret 4: single player + g_campaign 1 -> mode 7 (Underworld import, level 25+)
# cUI_NetGranny: 3D hero view (game/ui/granny.h)
0x006f06f0 cUI_NetGranny::cUI_NetGranny # (rect by value), ret 0xc
0x006f17f0 cUI_NetGranny::Render # vf44 (device, 0, label), ret 0xc
0x006f4b20 cUI_NetGranny::SetHero # (hero info, x, y, z, turn), ret 0x14
0x006f4ae0 cUI_NetGranny::ClearHero
0x006f0f10 cUI_NetGranny::SetLights # (on): +0x53f white set
0x006f4d70 cUI_NetGranny::InitCamera
0x0070beb0 cUI_Character::LoadExportedHeroes # Hero00..07.pax -> slot grannies +0x1b4
# Textures by name (game/ui/texture.h)
0x00656c80 TextureManager_Instance # cdecl (), kept at 0x13e57b4
0x00657620 TextureManager::IdOf # (name), ret 4
0x0065f010 TextureCache::Get # (id, 0), ret 8; cache at 0x13e57b8, D3D surface +0x14
# Main menu (mods/screen.cpp)
0x007122c0 cUI_MainMenu::Render # vf14
0x0075df10 MenuScene_Instance # 0x1a84 bytes, kept at 0x17ecb40
0x0075e620 MenuScene::Render # (1 argument), ret 4: sky + water (UI_UW_MAIN_CENTER, UI_MAIN_REF)
0x0075e820 MenuScene::Update # (seconds)