- Creatures were culled beyond 1024+200 x 768+200 screen pixels (cmp 4C8h / 3C8h in both object passes): invisible in the right part of a wide view, and the player too once the centre passed 1224. The bounds follow the render size now. - Centre immediates written as negatives (-200h / -180h), missed before: the row renderer's static object origin (0x62babb) and aiming at the cursor (0x4fb620). - HUD: moved windows no longer shift the shared mouse cursor object; the cursor accessor (0x6550f0) hands them a per-thread shifted copy and writes back what they changed. World input on other threads read the shifted position now and then, which made hold-to-move jerky. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
7.6 KiB
Widescreen: how the game sizes its view
Goal: render the world 1:1 at the monitor's native resolution, showing more of the world instead of scaling the 1024x768 image. Render and activity (animation/AI) distance must grow with it. UI keeps its 1024x768 layout.
Frame (render thread)
cEngine::renderThread loop, 0x0060e3f0, per frame:
- clear (D3D device
+0x28) - updates:
0x401660,0x640b50,0x634210, weather0x417e50/0x41a4d0 - world:
view->vf14(device)whereview = engine[+8 + idx*4](cWorldView0) - UI:
0x756820()->0x7587b0(device), debug overlays0x60f3e0/0x60f500 - FPS overlay
0x60aa90,DxDriver7::Flip
Skipped entirely while g_appFlags & 0x20000 (app inactive).
World view (cWorldView0, vtable 0x008900f8)
| Offset | Meaning |
|---|---|
+0x14/+0x18 |
camera position (world pixels) |
+0x1c/+0x20 |
requested camera position (scroll target) |
+0xb60 |
zoom (float, clamped to 0.5 .. 2.0) |
+0x96ab0 |
view width = zoom * 1024.0 |
+0x96ab4 |
view height = zoom * 768.0 |
+0x96ac0 |
extra rows (4) |
+0x96ac8 |
columns = width/96 + 12 |
+0x96acc |
rows = height/48 + 24 |
Functions:
| Address | Name | Notes |
|---|---|---|
0x00624ae0 |
cWorldView0::SetupView |
computes the size fields above from zoom and the float constants 1024.0 (0x0088e6f4) and 768.0 (0x00890040); tile corner offsets ±48.2/zoom, ±24.2/zoom |
0x006322b0 |
cWorldView0::Render (vf14) |
iso tile grid: tiles 96x48, two staggered half-rows per row (0x62b000), first tile = camera - (width/192, height/96) |
0x0062a730 |
cWorldView0::UpdateCamera (vf0c) |
scroll blocking against sector edges uses width/2, height/2 |
0x00628210 |
cWorldView0::SetZoom |
clamps zoom, sets the ortho projection for 3D models: x ±267zoom, y ±200zoom, z -600..2500, then calls SetupView |
0x00623c40 |
world -> screen projection | x * 1024.0 / (2/P00), y * 768.0 / (2/P11) |
0x006426a0 |
build ortho matrix | (l, r, b, t, n, f) |
All tile/screen positions are pre-transformed (FVF 0x1c4: XYZRHW, diffuse,
specular, tex1); 3D models use the ortho projection.
Users of the 1024.0 / 768.0 float constants
World view (should follow the new size): 0x61e5c2, 0x61e5ec, 0x623c82,
0x623c9c, 0x624af2, 0x624b03, 0x628f65, 0x628f8f, 0x629294,
0x6292bc, 0x62b43a, 0x62dc32, 0x62ea2b, 0x62ea5e, 0x62f30d,
0x62f345, 0x630656, 0x63068a, 0x630f3f, 0x630f77 (mostly on-screen
clip tests).
Elsewhere (unknown / UI, leave alone for now): 0x40694a, 0x4ec036,
0x593c25, 0x593c3a, 0x6ccb87, 0x6ce156, 0x758a34, 0x75f79a,
0x75f7b8.
Ortho projection extents (±267 / ±200 pushed as floats)
0x60c1f7, 0x60da54, 0x625039, 0x6283b3, 0x62858d, 0x628d3b.
Other frame producers (laid out for 1024x768: "classic" frames)
| Address | What | Thread |
|---|---|---|
0x00756940 |
cUI_Manager thread loop: main menu (clear, update, BeginScene 0x646f50, UI render 0x7587b0 incl. the menu's 3D scene, EndScene 0x646fd0, Flip) |
own UI thread |
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
BeginScene when the scene depth +0x1d8 is 0) and the borderless mod
presents only that area, scaled to the monitor.
Direct3D 7 render target limit
Windows' D3D7 runtime refuses IDirect3D7::CreateDevice on any render
target wider or taller than 2048 (DDERR_INVALIDOBJECT, 0x88760082),
measured with a standalone test: 2048x1440 works, 2049x1440 / 2560x1080 /
3440x1440 fail. The default render size (768 high, e.g. 1835x768) stays
below it; larger sizes are clamped to 2048. (A D3D7 -> D3D9 wrapper,
dxwrapper, lifted the limit but was dropped again in commit history once the
render size no longer needed it; it also required fixing the device info GUID
pointer at +4 of SetDisplayMode's first argument, which its enumeration left
invalid.)
Renderer
DxDriver7::SetDisplayMode (0x00645390) creates back buffer, z-buffer and
D3D viewport from DxDisplayMode::width/height (hardcoded 1024x768 by
InitGame, 0x00815620, which also calls ChangeDisplaySettings(1024x768)
in fullscreen).
Fixed 1024x768 elsewhere (later)
0x00613710engine capture (save-game thumbnail),0x006483f0TGA writer0x0060e3f0GDI BitBlt of a 1024x768 bitmap (startup/movie path)0x00627260name/HP labels while Alt is held (no constants, uses view)
Render size
Default: 768 world pixels high (the original vertical view), width from the
monitor aspect (3440x1440 -> 1835x768), scaled to the monitor by the
borderless mod. Everything keeps the on-screen size players know; the view
only grows horizontally. [widescreen] Height=0 renders natively 1:1
(everything about half size on a 1440p monitor).
Things that had to follow the render size
- view size / centre / clip floats and their integer immediates (world view)
- tile draw list and ground quad list (fixed 1750 / 1749 entries) moved into an enlarged view object
- 3D ortho extents: world (+-267 x +-200, zoomed and unzoomed) and cGranny (+-512 x +-384, incl. cUI_NetGranny for HUD items)
- creature clip test in both object passes (
0x62f988,0x6315cb): creatures are drawn only within -200 .. 1024+200 x -200 .. 768+200 screen pixels (cmp 4C8h/cmp 3C8h); unpatched, creatures in the right part of a wide view were invisible, and above ~2450 px width (centre > 1224) even the player - centre immediates written as negatives (
add reg, 0FFFFFE00h,lea [..-200h]), missed by a search for 200h: the row renderer's static object origin (0x62babb) and aiming at the cursor (0x4fb6ef) - the inverse projection
0x623a20(screen -> view) uses 1/1024 and 1/768: game logic (camera follow0x44d9f0) converts world -> screen -> view with the device's current projection; unpatched it sent the camera off-world
Screen-space overlays
- Alt object labels (
0x627260) and floating texts / damage numbers (32 slots, drawn from the render loop at0x60e940via0x41e460) are packed without overlap into a screen-sized area (0x662300,push 300h / 400h): widened to the render size (kLabelAreaPushes). - More world -> screen conversions with integer centre immediates
(
x - camera + 200h): the light/effect list at+0xce14(0x41b1xx) and the floating texts (0x41e560, inverse0x41eaf0). Unpatched, lights were offset from the player. - Left alone:
push 300h / 400hat0x4cc7d3,0x5d7bb7(1024x768 textures),0x60e869(GDI BitBlt),0x613859..(screenshot pixel conversion), and 200h/180h pairs at0x52bab7,0x52c6c0,0x6c...-0x72...(UI windows, 1024 layout).
Activity (simulation) distance
The world is split into sectors of 64x64 tiles (cSector, manager
**0xad3bc0, sector pointers at +0x240, flags +0x30: 0x10000 loaded,
0x800 active, tile origin +0x44 / +0x48). The 3x3 sectors around the
player's sector are active (Sector[%4d] Active (3x3 sectors), test
0x638ff0); about 80 are loaded. Measured in game: 9 active sectors, origins
64 tiles apart.
With 96x48 px tiles, a sector spans 6144 px horizontally and 3072 px vertically, so the active block reaches at least that far beyond the player's sector in every direction. The widened view needs at most +-1835 x +-768 px (1835x768 at zoom 2), so everything visible is simulated; nothing to change.
Open questions
- Fixed-size per-view buffers (e.g.
+0x80e40, 5 vectors at+0x96ad0with capacity 0x200) - do they overflow at 3440x1440 (47 columns x 54 rows)?