which side panel(s) open: 0=none,1/2/3=side/both; drives the L/R world-view shift; == nScreenPanels passed to the driver tile-draw fns


offX: 0 with no panel, +/-DispW/4 (=320) with one; the native left-shift the mod KEEPS


*[this] = the render ctx (ONE struct): draw-window +0x04(L)/+0x08(T)/+0x0C(R)/+0x10(B), scroll +0x24/+0x28, dirty +0x00, visited-room grid +0xEA9C


DispW=1280 (SGD2FreeRes). +4=height, +8=w-copy, +0xC=centerX, +0x10=centerY


per-panel open flags: inventory +4, char +8, skill +0x10, waypoint +0x50


*[this] = the active video-driver dispatch struct (= D2Glide's 0x6F8649F0); slot +0x9c = the cel handler D2Client #10001 invokes


driver DisplayWidth; SGD2FreeRes rewrites 800->1280 at RVA 0xDCF1 at runtime; the -/4 math survives -> 960


driver DisplayHeight; stock 600, SGD2->720


[this]+4 / [this] gate the worker 0x7080: both-nonzero -> DEAD software drawer 0x6290; else -> LIVE drawer 0x6840


the driver vtable D2gfx installs at [0x6FA91268]; +0x9c=0x6F8572F0 (cel handler)


D2Glide IAT slot for glide3x!grDrawVertexArrayContiguous@16 (into D2DX). 0x6F856032 = jmp [this]. THE D2Glide->D2DX crossing; hook target for the pipeline-trace probe


main world render entry. body: PASS1(0x44302) -> tilesub(0x44307) -> PASS2(0x4436F, gated mode!=3) -> compositor(0x4437B) -> present #10040(0x443B9) -> RET(0x443BE)


floor pass wrapper; esi=ctx, [esp+4]=bTilesOnly, ret 4; calls DrawRooms


wall/roof + unit pass; eax=CullingSpec; gated cmp[ScreenOpenMode],3;je


builds room-iteration RECT from ctx draw-window+scroll (band=[scrollX,scrollX+(R-L)]); per-tile cull skip if tileWorldX>RECT.R; grid helper 0x6FB2A0D0 sizes ctx+0xEAA8 (HEAP UAF trap on save/restore)


floor tile flush; screenX=ctx+0x04-ctx+0x24+tileAbsX; NO inline clip; hands each tile to the cel blit thunks


jmp [IAT 0x6FB7EC3C] -> D2gfx #10001. TILE-DEDICATED; the TILEBAND/GROUND-COLLECT counter hook site


jmp [IAT 0x6FB7EC40] -> D2gfx #10058 (alpha)


mov eax,[0x6FA91268]; call [eax+0x9c]; ret 0x14 -- thin forwarder into the driver cel handler, no clip


solid fill; forwarder to driver vtable +0x84; 295 D2Client callers, none targets [960,1280] (overdraw ruled out)


the ONLY export: mov eax,0x6F8649F0; ret -- hands D2gfx the driver dispatch struct


pfTileDrawLit (dispatch struct +0x9c); the live cel handler D2Client #10001 reaches; calls worker 0x7080


gates on [0x6F865A8C]: null -> LIVE drawer 0x6840; non-null -> DEAD 0x6290


THE live cel->vertex drawer (confirmed fires: [GLIDECULL] edx=ScreenOpenMode). Per-subtile cull vs [esp+0x30]left,[esp+0x38]right narrowed by panel-mode (mode1 right=DispW/2=640 @0x6889, mode2 left=DispW/2-0x20=608 @0x689E). v1.9.1 NOP'd both -> NO visual change => this cull is world-space, NOT the screen clip. Submits via grDrawVertexArrayContiguous


DECOY -- DO NOT HOOK. Software/LFB drawer reached only via the non-taken branch of worker 0x7080; PROVEN dead (v1.9.0 hook never fired). Computes DisplayWidth-DisplayWidth/4=960 but never executes -- the 960 was mis-inferred off this dead code


DECOY -- DO NOT HOOK. Uncalled twin of 0x6290 (0 E8/dword refs); identical width math; register-arg convention. Dead


detour the base `call PASS1` to widen ctx+0x0C->DispW before PASS1 (issues floor tiles to screenX ~1236). NECESSARY but not sufficient (strip still black -- downstream clip)


NOP the mode-1 right-narrow store in the live drawer. v1.9.1: applied cleanly, NO visual change -> the cull isn't the screen clip. FAILED attempt


NOP the mode-2 left-narrow store. Same v1.9.1 FAILED result


D2DX Glide-vertex submit = the D2Glide->D2DX crossing. PROBE v1.10.0 (filtered to the tile drawer by return-addr): floor geometry crosses at max screenX 1296, ~138 verts/frame in dead-zone [960,1280). The world geometry REACHES D2DX past 960 yet renders BLACK -> the panel-world clip is IN D2DX (m2 was right).


D2DX Metrics.cpp: desktop 1280x720 -> gameSize={960,720} (narrow), gameSizeWide={1280,720}. 960 = the narrow render target. Chosen by GetSuggestedGameSize(desktop, wide=!NoWide) (D2DXContext.cpp:1170); d2dx.cfg nowide=false + [game]size=[1280,720] should force 1280 -- CONFIRM at runtime.


SurfaceIdTracker.cpp:88: ScreenOpenMode&1 (panel open) && minx>=gameSize.width/2 (=640) RETAGS a world batch surfaceId=USER_INTERFACE. Prior audit said 'only tags, no cull' -- but the visible world clips ~640 with a panel open, so re-examine whether the UI-tag changes world compositing/clipping. Prime suspect.


2026-07-12 (v1.10.0 probe): the panel-world dead-zone clip is IN D2DX. Floor geometry crosses grDrawVertexArrayContiguous at max 1296 (138 verts in [960,1280)) yet the dead zone is BLACK. NOT D2Client (issues 1236), NOT D2Glide (submits past 960). The [640,960] tiles render via the SAME path, so the [960,1280] verts are valid, not degenerate -> D2DX clips them. Fix belongs at D2DX via a runtime glide3x hook from esoteric.dll (rebuild not available here).


D2DX grSstWinOpen (export _grSstWinOpen@28). OnSstWinOpen consumes _customGameSize as gameSize (D2DXContext.cpp:176-180) then creates/sizes the RenderContext. THE fix hook site: detour entry, call SetCustomResolution(1280,720) before delegating so the world target opens WIDE. Prologue 55 8B EC 6A FF (push ebp;mov ebp,esp;push -1).


exported _D2DXGetConfigurator@0 (NOT @4 -- the header D2DXConfigurator.h:98 string is a bug; use @0 or ordinal 1). Returns ID2DXConfigurator*; vtable [0]QI [1]AddRef [2]Release [3]SetCustomResolution(w,h) __stdcall. SetCustomResolution sets _customGameSize (a flag consumed at the next grSstWinOpen).


v1.11.0 THE FIX: detour grSstWinOpen; before each open call ID2DXConfigurator::SetCustomResolution(1280,720) so D2DX opens its WIDE game target (1280 vs the narrow 960 that clipped the world). Installed by a thread that waits for glide3x (late video driver). glide3x relocates -> resolve by export/ordinal. Keeps the native left-shift (it's in the submitted vertices). Awaiting Doug's test.


2026-07-12 (workflow d2dx-panel-clip-fix, source+bytes): D2DX renders the world into its NARROW _gameSize={960,720} (Metrics.cpp desktop 1280x720). Clip cutoff == 960 == _gameSize.width: framebuffer sized to gameSize (SetFramebufferSize), UpdateViewport sets RSSetViewports+RSSetScissorRects to that rect, GameVS.hlsl NDC.x/960 -> x>=960 off-RT+out-of-scissor+NDC>1, rasterizer-clipped. The SurfaceIdTracker retag (minx>=gameSize/2) is a RED HERRING (surfaceId only feeds FXAA, color write unconditional). Fix = force gameSizeWide via SetCustomResolution(1280,720).


[SUPERSEDED 2026-07-12: the 640 was the CULL with GlideUnclamp=0. Gated probe with GlideUnclamp=1 -> submit 1264, 338 verts in the dead zone, still black => the binding clip is a D2DX-downstream DROP, not D2Glide 640.] CORRECTION 2026-07-12 (v1.10.0 probe, PANEL-OPEN frames): with a panel open, the D2Glide live drawer 0x6F856840 submits floor geometry to grDrawVertexArrayContiguous at max screenX ONLY 640 (=DisplayWidth/2, the mode-1 right bound), band[960,1280)=0. D2Client still ISSUES to 1280 (TILEBAND). So the binding panel-open clip is D2Glide 0x856840 at 640, NOT D2DX. My earlier 'crosses at 1296 -> D2DX' read used NO-PANEL frames (before [WIDEN] mode=1) by mistake -- the harness was right, I read the wrong frames. finding.panelworld_clip_in_d2dx and finding.d2dx_gamesize_960 describe the NO-PANEL/second clip; the panel-open lever is here. Likely TWO clips: D2Glide 640 (panel) + D2DX 960 target; both need fixing. v1.9.1 cull-NOP showed no visual change -> re-testing GlideUnclamp WITH the probe to see if it widens the submit past 640 (cull-skip) or the 640 is a vertex-X clamp elsewhere in 0x856840.


Panel open: is the D2Glide floor submit clamped at 640(=DispW/2) by the [esp+0x38] per-subtile CULL (NOP-able @0x856889) or by a VERTEX-X CLAMP elsewhere in 0x856840's projection? v1.11.2 gated probe (GlideUnclamp=1) settles it: submit max ~1280 => cull is the lever; ~640 => clamp is elsewhere.  ==> ANSWER: It's the CULL. v1.11.2 gated probe (mode=1): GlideUnclamp=1 -> panel-open D2Glide floor submit max 1264-1296 with 338 verts IN [960,1280); GlideUnclamp=0 was 640. The NOP defeats the cull (640->1264). The remaining black is a DROP at/after D2DX -- geometry reaches the dead zone, D2DX doesn't paint it.


Why did v1.9.1/v1.11.0 GlideUnclamp (NOP the cull narrowing 0x856889/0x85689E) show NO visual change if 640 is the cull? Either the NOP widens the submit but a downstream layer hides it, or 640 comes from a clamp the NOP doesn't touch. Depends on OQ.panelopen_640_cull_vs_clamp.  ==> ANSWER: GlideUnclamp DOES widen the submit (640->1264, 338 verts into the dead zone) -- never 'no effect'. The visual didn't change because the widened geometry is dropped downstream at/after D2DX.


DISPUTES finding.d2dx_gamesize_960. What is D2DX's ACTUAL runtime gameSize in-game (panel open)? Workflow inferred 960 (narrow). But forcing 1280 via SetCustomResolution (D2DXWiden) SHRANK the menu (black margin) -> implies native was LARGER than 1280, contradicting 960. Need a runtime read (probe gameSize / grSstWinOpen args), not source inference.


Units/NPCs shift LEFT on inventory-open (they must NOT shift). Source unknown: the ctx+0x0C widen, D2DXWiden (now reverted), or the double-motion-smoothing (d2fps+D2DX both on). Re-check with D2DXWiden off.  ==> PROGRESS 2026-07-12: terrain shift CONFIRMED = -320 (1x) via offX(0x6FBCC418)->ctx+0x04; scrollX untouched. Shared tile projection is 1x for all layers, so the objects' extra -320 is downstream/unit-specific. STILL OPEN: unit-layer re-read of offX (theory a) vs motion-smoothing (theory b). NEXT: xref offX reads -- if the unit/PASS2 path reads 0x6FBCC418 too, theory (a).  ==> 2026-07-12 xref: offX read 173x, 1 write (0x6FB5AF7C). Attempt A (offX=0) un-shifted floor+units but NOT walls (separate source) + broke clicks. Shift is MULTI-SOURCE + hit-test-coupled; units lean theory (a). Naive offX-kill is OUT.


ONE clip (D2Glide 640) or TWO (D2Glide 640 + a D2DX target)? Panel-open probe=640, no-panel probe=1296 => the D2Glide clip is panel-mode-dependent. Whether D2DX ALSO clips once geometry passes 640 is blocked by OQ.d2dx_runtime_gamesize.


D2DXWiden(1280) regressed the menu resolution. Is forcing 1280 wrong for the low-res menu, or is the game's native size >1280? Ties to OQ.d2dx_runtime_gamesize. If D2DXWiden is re-applied, scope it IN-GAME only.


Council wave-4 (chat m5 + clau m6 converge): the panel-open dead zone is the 960-WIDE RENDER-TARGET EXTENT, not a surfaceId routing/split. D2Glide announces 960 gameSize to D2DX at grSstWinOpen; SGD2FreeRes widened D2 to 1280 without updating that; D2DX allocates a 960 world RT and clips the 1280-coord floor. Fix they gave = D2DXGetConfigurator->SetCustomResolution(1280,720) before grSstWinOpen. STATUS: we BUILT this (feature_d2dx.c / D2DXWiden) -> 122 successful calls but it REGRESSED the menu res and did NOT fill. So their consensus fix is tested-and-insufficient.


Council wave-4 dissent (gem m4): the drop is the UI-RETAG. D2DX SurfaceIdTracker:88 retags world batches with minx>=640 as USER_INTERFACE when a panel is open; retagged verts get the UI orthographic projection (wrong for world coords) and render into the void = black. Fix he gave = hot-patch the minx>=640 Jcc in glide3x to fail ONLY during the floor draw call, then restore. NOTE: clau m6 REFUTES this - [640,960) is retagged too yet renders fine, so a rule that fires on visible geometry can't be the blackout rule.


Council wave-4: BOTH chat m5 and clau m6 demanded a PRE-PATCH measurement we never ran -- read D2DX's ACTUAL D3D11 world render-target width + viewport (RSGetViewports width; OMGetRenderTargets->GetDesc width/height) at the first Game-pass Draw, plus grSstWinOpen incoming args. chat's decision tree last branch: 'viewport 1280, RTV 1280+, constants 1280, still cutoff at 960 => your binary differs; next hook is ID3D11DeviceContext::Draw, inspect bound VS/PS and MRTs on the first missing floor batch' -- NOT SurfaceIdTracker.


DISAMBIGUATOR (council chat+clau demanded, never run): what is D2DX's ACTUAL D3D11 world render-target width + viewport, panel-open vs closed? Pairs with the panel-gated D2Glide submit probe to triangulate the stop point: (submit 640 + RT 1280)=>upstream D2Glide; (submit 1296 + RT 960)=>D2DX extent; (submit 1296 + RT 1280)=>wide-but-retagged (inspect MRTs). Build: hook ID3D11DeviceContext::Draw or read D2DX gameSize; or log grSstWinOpen incoming res arg (feature_d2dx already hooks it).


Doug reframe 2026-07-12: the dead zone, the panel-open camera SHIFT, and the world-vs-units DESYNC are ONE cause -- opening a panel CHANGES THE WORLD VIEW-STATE (offX=-320 + play-area narrowing). Clean target: render the world in the NO-PANEL view-state (no shift, full width) and composite the panel on top = ONE view-state (matches the ledger META-LESSON). Neutralize the change AT ITS SOURCE (the panel-open handler 0x6FB5AEC0 writing offX @0x6FB5AF7C), leaving the mode flag 0x6FBCC414 intact for culling (attempt A proved mode is load-bearing).


RETIRED CONSTRAINT: 'keep the native -320 left-shift' (offX=0x6FBCC418, written @0x6FB5AF7C). Preserved through attempts A-H. Doug 2026-07-12 REVERSES it: wants ZERO shift on panel-open (world stays put, panel composites on top). 'both moves are inappropriate when opening the inventory if we have the world behind as we want.'


Doug confirms 2026-07-12: panel-open, the WORLD shifts HALF as much as characters/items (units = ~2x world). Ledger theories (both unconfirmed): (a) multi-layer shift (floor vs wall/roof/units = separate draw fns), (b) double motion-smoothing (d2fps + D2DX both interpolate the camera move -> 2x on moving units, 1x on static terrain). Exactly-2x favors (b). MEASURE before patch: log offX(0x6FBCC418) + ctx+0x24 world-scroll + one unit screenpos, panel-open vs closed.


CONFIRMED 2026-07-12 (v1.11.2 panel-gated probe, mode=1, GlideUnclamp=1): floor geometry REACHES the dead zone at the D2Glide->D2DX seam -- submit max 1264-1296, 338 verts in [960,1280) -- yet renders BLACK. The dead-zone black is a DROP at/after D2DX, NOT a D2Glide clip (GlideUnclamp already carries the submit to 1264). Independently reproduces council wave-4 (geometry crosses into D2DX in the dead zone, black). Next diagnostic = the council's demanded D2DX world-RT-width read (extent 960 vs UI-retag). SEPARATE from the world/objects SHIFT desync.


CONFIRMED 2026-07-12 (v1.11.3 [SHIFT] snapshot, standing still): opening a side panel shifts the FLOOR/terrain by exactly -320 (1x = -DispW/4), and the WHOLE shift is offX(0x6FBCC418) 0->-320 driving ctx+0x04 0->-320. scrollX(ctx+0x24) is NOT touched by the panel (log changes there = the player walking). Terrain pan (ctxL - scrollX) moves -320. So the terrain-shift LEVER is offX->ctx+0x04. The objects-2x is a SEPARATE source: the shared tile projection is only 1x, so units (PASS2, a separate draw fn) pick up an EXTRA -320 downstream, NOT the camera/ctx. Which source (unit re-read of offX vs motion-smoothing) is unmeasured.


CONFIRMED 2026-07-12 (d2re xref 0x6FBCC418): offX is READ at 173 sites across D2Client, but WRITTEN once (0x6FB5AF7C, =-DispW/4=-320 on panel-open). So the surgical lever is the single write, not the 173 reads (blanket-patching reads = ledger attempt A = infeasible). CROSS-REF attempt A (held offX=0): un-shifted FLOOR + UNITS but NOT walls/roofs (a SEPARATE shift source) AND broke click-to-move (the hit-test reads offX). CONCLUSION: the panel shift is MULTI-SOURCE (offX for floor/units; a different global for walls/roofs) and HIT-TEST-COUPLED. Units read offX twice -> the 2x (theory a favored; not proven vs motion-smoothing). Naive offX=0 is OUT (already failed). The reframe goal (render world in no-panel view-state) needs an architecture decision, not a single patch.


Council wave-5 (all 3 CONVERGE): kill the panel shift by AUTHORING a coherent NO-PANEL WORLD VIEW-STATE for the whole world-draw -- NOT per-value patching, NOT a mid-frame ctx swap (that's the C-G scheduling trap: 173 readers read at different times, no single safe instant). Two flavors: m1(chat)=post-hook the panel-transition routine (holds offX write 0x6FB5AF7C); after it runs, canonicalize the world-camera outputs (offX=0, wall/roof offset=0, ctx+0x04=0, ctx+0x0C=DispW) and HOLD persistent; keep ScreenOpenMode=1/2 (UI+input stay live). m2(gem)+m4(clau)=STATE-SPOOF: hook the world-draw, temporarily set the panel-state flags to 0 (lie 'no panel open'), call original (D2 naturally zeroes offX + wall offsets + hit-test), restore flags before the panel compositor 0x6FB739E0 -> ONE hook, hit-test solves itself, UI intact. Hit-test coherent BY CONSTRUCTION (render + mouse ray read the same authored state).


Council wave-5 CRITICAL LINK (m2 gem): the SHIFT fix and the dead-zone BLACK are the SAME downstream issue. Unshifting the world pushes valid geometry into x>=640; D2DX SurfaceIdTracker:88 retags world batches minx>=640 as USER_INTERFACE while a panel is open -> the UI orthographic projection drops those world verts -> black/corrupt right half. So authoring the no-panel view in D2Client is NECESSARY BUT NOT SUFFICIENT: we must ALSO defeat the D2DX minx>=640 retag during the world draw (runtime hot-patch the compare in glide3x, restore after). Defeating that retag IS the fix for the black.


BLOCKER (council wave-5 m1+m4): walls/roofs shift via a SEPARATE source, NOT offX (attempt A: offX=0 un-shifted floor+units but not walls). The authored no-panel view-state is INCOMPLETE until this source is named. Find it: read-breakpoint on the wall projection input during PASS2 (0x6FB3B110), or resolution-differential (vary DisplayWidth, scan for the value that tracks it AND encodes panel-open). Last unknown before the view-state-authorship patch.  ==> TESTING (v1.12.0 attempt.I_noshift_lens): authoring ctxL=0 will show if walls read the ctx (un-shift) or a PASS2 recompute (stay shifted).  ==> CLOSED (v1.12.0): the walls UN-SHIFTED with our ctxL=0 authoring -> the walls read the render context, NOT a separate source. Authoring the no-panel lens covers them.


The Esoteric Path — verified vs the 1.13c binary

The world render context. Instance pointer = *0x6FBCC3C4. It IS the world view-state: the tile projection reads screenX = (+0x04) - (+0x24) + tileAbsX, screenY = tileAbsY - (+0x28) + (+0x08). A side panel writes +0x04 = offX = -320 (the shift) and narrows +0x0C to the play-area edge (the clip). See [[finding.terrain_shift_is_offx_ctxL]] and [[reframe.one_cause_view_state]].

dirty

geometry-dirty bit; |=1 forces mirror/cull rebuild for a wider window

draw-window L; equals offX (0 no-panel, -320 panel-open) -> the SHIFT

draw-window R; iteration bound = 1280 no-panel, 960 panel-open -> the CLIP

scroll origin X (camera); NOT touched by the panel


D2 community headers — D2Ex2/D2Structs_113D.h (jankowskib) + d2bs (noah-)

The universal client-side game-entity struct; every player, monster, item, and object shares this layout, with a union selecting the type-specific data block.


Per-player data block (name, quest flags, waypoints, assigned/switch skills, trade state) hung off UnitAny.pPlayerData; copied from the serverside layout.


Per-monster data block (txt record, gfx components, name seed, unique/champion/minion bit-flags, enchantments, AI pointer, name) hung off UnitAny.pMonsterData.

(anonymous bitfield)

fOther, fSuperUniq, fChampion, fUnique, fMinion, fPossesed, fGhostly, fMultishot (each 1 bit)


Per-object data block (object txt record, interaction type, owner name) hung off UnitAny.pObjectData.


Per-item data block (quality, seeds, owner, flags, prefixes/suffixes, body & inventory location, socket linkage) hung off UnitAny.pItemData.

BodyLoc

location on body = xpos of item (only if on body) (spread)

InvPage

0 = inventory, 3 = cube, 4 = stash (spread)


Movement/position path for an active (moving) unit: current and target coordinates, owning rooms, collision flags, path type, target unit, and facing direction.

yUnknown

8 * (wInitX + wInitY + 1) <- Mby AutoampY

dwCollisionFlag

0x1804 <- bFlying, 0x3401 <- bOpenDoors, 0x3C01 <- Cannot Open Doors, 0x804 <- Ghost, 0x1C09 <- Player


Simplified position record (size 0x20) for units that do not move (items/objects): owning room plus offset and grid position.


Skill container hung off UnitAny.pInfo: mem pool plus pointers to the first, left-hand, and right-hand active skills.


A single skill entry in a unit's skill linked list: txt record, next-link, mode, level, and flags.


Skill identifier record referenced by Skill.pSkillInfo; holds the skill id. NOTE: absent from D2Structs_113D.h (which uses SkillsTxt* directly) — this definition comes only from the d2bs D2Structs.h.


Active/in-memory room node holding collision map, units, and nearby-room links for a level.


Static/preset room descriptor (DRLG layout) linking to its Room1, level, tiles, and preset units.

struct { DWORD dwRoomNumber; DWORD* _1; DWORD* pdwSubNumber; }*

inline anon struct: dwRoomNumber //0x00, _1 //0x04, pdwSubNumber //0x08. points to 0x70 struct if dwPresetType == 1, 0xF8 struct if == 2


A single DRLG level (map area) with its rooms, size/position, seed, presets, and warp/room-center coordinates. sizeof = 0x230.

(anonymous union)

union { LvlMazeTxt* pMazeTxt; PresetData* pPreset; WildernessData* pWilderness; }

pMazeTxt for dwDrlgType==1 (RANDOM MAZE); pPreset for dwDrlgType==2 (PRESET MAP); pWilderness for dwDrlgType==3 (RANDOM AREA WITH PRESET SIZE)

union { DWORD RoomCenterX[9]; DWORD WarpX[9]; }

union { DWORD RoomCenterY[9]; DWORD WarpY[9]; }


Per-act runtime container holding the act's environment, town level, map seed, root room, and DRLG misc pointer.

not sure, some inline struct of 0x30 size seems fit


DRLG-level misc/generation state for an act (aka Drlg): seeds, tile pointers, callbacks, level list, and back-pointer to Act. sizeof(0x48C).


A preplaced (preset) unit within a Room2 — class/type id and tile position, linked as a list.


A tile linking a Room2 to a neighboring room (warp/connection), stored as a linked list.


A single stat entry: sub-index, stat index, and value.


A stat vector/array container: pointer to a Stat array plus counts (size 0x8).


A node in a unit's linked list of stats (size 0x40).


Extended stat list container tracking base/full/mod stats for a unit or item (size 0x64).

pLastList

pointer to the last pStatList of the StatListEx owner (aka item owner in case list ex belongs to item)

pListEx

pStatListEx - pointer to owner StatListEx (if this one is owned by a item, this points to the item owners list)


A unit's inventory: item linked list plus cursor/owner/count data.


Layout/geometry of an inventory grid panel (slot sizes and screen bounds). Not present in D2Structs_113D.h; taken from d2bs D2Structs.h, which has no offset comments.


A single frame/cell of a DC6/DCC sprite graphic (dimensions, draw offsets, pixel data length).


Rendering context binding a unit/component to its cell file and current graphics cell and facing direction (size 0x48).


Graphics draw-info block holding the draw-offset table pointer and palette-shift index for a sprite.


A dynamic light source in the game world (type, static lightmap validity flag and map pointer). Only a forward declaration exists in 113D, so the d2bs body is used.


A layer of the automap holding linked lists of floor/wall/object/extra cells (sizeof = 0x1C). Verbatim source spelling is AutoMapLayer.


A single automap tile node (cell number, pixel position, weight) linked as a binary tree via pLess/pMore. Verbatim source spelling is AutoMapCell.


A UI control/widget (button, textbox, etc.) with position, size, handler pointers, linked text and edit state. Only a forward declaration exists in 113D, so the d2bs body is used.

unsure? definitely a ptr but not obvious, usually points to 6 when dwType is 6 I think

some sort of function (maybe click?); preceding source comment: I think _2 thru _9 are a handler table of some sort

another random ptr... mostly dead ends when I examined them

unkState

_11 0 when button avail to be clicked 1 when greyed - still need to look at this more

dwCursorPos

union member, anonymous struct: Textboxes (no offset comment in source)

union member, anonymous struct: Buttons (overlaps textbox members at 0x5C)


A single waypoint activation record (flags bitfield). Only a forward declaration exists in 113D, so the d2bs body is used.


# The Esoteric Synthesis — D2 is a graph projected through one lens

*Our own synthesis, fusing the ingested canon with what we verified against the binary. This is the
"make it ours" layer: the basic facts assembled into the single architecture that explains our whole hunt.*

## 1. The graph (what exists) — screen-independent, seed-deterministic
`Game` owns everything. The spine is `Game -> Act -> Level -> Room2 -> Room1 -> UnitAny`.
- **Room2** is the static, seed-deterministic *description* of a room; **Room1** is the live *instance*,
  hydrated only near players. World state is data, not pixels.
- **UnitAny** is the universal entity: a tagged union with three arms -- **A** `pUnitData` (*what it is*:
  Player/Monster/Object/Item), **B** `pPath -> Room1 -> ... -> Act` (*where it is*), **C** `pInfo`/skills
  (*what it can do*). Stats (`pStats -> StatListEx -> Stat[]`) and inventory (`pInventory -> item UnitAny
  list`) are a **parallel graph off the same root** -- same UnitAny, disjoint bodies, meeting only at the screen.
- Nothing in this graph stores a screen coordinate. Ever.

## 2. The lens (how it reaches the screen) — one render context per frame
Every tile and unit is projected through ONE struct we verified, [[struct.D2Client_RenderContext]]
(`*0x6FBCC3C4`): `screenX = draw_left(+0x04) - scroll_x(+0x24) + worldX`. The renderer walks the Arm-B
spine (`Act -> Level -> Room2 -> Room1 -> unit list`), turns each entity's room-space Path position into a
`CellContext.dwPosX`, and blits its `GfxCell` through `D2Client -> D2gfx -> D2Glide -> D2DX`. The lens is
the *only* place world->screen happens.

## 3. The phenomenon (the theory that explains everything we've chased)
Opening a panel **does not touch the graph** -- it mutates the **lens**:
- `offX -> draw_left(+0x04) = -320` -> the **shift** ([[finding.terrain_shift_is_offx_ctxL]])
- `draw_right(+0x0C)` narrowed to ~960 -> the **issue-clip**

So the shift, the clip, and the world/units desync are **one cause**: a view-state mutation
([[reframe.one_cause_view_state]]). The graph is innocent; the lens moved. Units take an *extra* -320 in
PASS2 (a second lens-read) -- that is the 2x desync. And the residual black is a **second lens downstream**:
even when we widen `draw_right` and D2Glide submits the far tiles (338 verts into [960,1280)), **D2DX drops
them** ([[finding.clip_is_d2dx_drop_confirmed]]). Two lenses, one graph.

## 4. Why this makes the fix tractable
Because the graph is screen-independent (the canon *proves* it), we can render the whole Arm-B walk in a
**no-panel lens** (offX=0, draw_right=DispW) and composite the panel on top -- safely, without touching the
173 offX readers or any world struct. The canon tells us it's safe; our RE tells us the exact two knobs
(the D2Client lens + the D2DX drop). That is the entire path to ZERO black with the world behind the panel.

**One line:** *D2 is a screen-independent object graph drawn through a single per-frame lens; every symptom
we've chased is the lens being rewritten when a panel opens -- never the graph.*


The Esoteric Path — canon synthesis (grounded + cited)

# The units struct web (D2 1.13c) and how it reaches our render path

Grounding: UnitAny field offsets are from the in-repo 1.11b Structure Reference (`research/kb/corpus/forum/highvalue/t47809.json`); the render-context side is from `grimoire/symbols.jsonl` (`struct.D2Client_RenderContext`, `D2Client.DrawRooms`, `D2Client.FloorFlush`). Offsets are stable UnitAny-relative through 1.13c. Room1/Room2/Level/Act are named by their canonical fields; the repo confirms the *roots* of that chain (see the lifecycle section) but not every byte offset inside it, so those inner offsets are named, not asserted numerically.

## 1. UnitAny is the hub — a tagged union with three "arms"

Every player, monster, object, item, missile and tile is one `UnitAny`. It carries a type/id header and then hands off to per-type payloads through three pointer arms:

- **Identity header**: `+0x00` dwType, `+0x04` dwTxtFileNo, `+0x0C` GUID (links a server unit to its client twin), `+0x10` eMode (anim/action mode), `+0x18` actNo, `+0x28` dwInitSeed.
- **Arm A — what it *is*** (`+0x14 pUnitData`, a union of 5 classes per the repo): `pPlayerData` / `pMonsterData` / `pObjectData` / `pMissileData` / `pItemData`. Which member is live is selected by dwType. "Note, there is no pTileData."
- **Arm B — where it *is*** (`+0x2C pPath`, a union of 2 classes per the repo): `pStaticPath` (Objects, VisTiles, Items) vs `pDynamicPath` = **Path** (Players, Monsters, Missiles).
- **Arm C — what it can *do*** (`+0xA8 pSkills`): the skill controller = **Info**, "a list of all skills the unit has (pointers to pSkill)."

Other hub fields that matter for lifecycle/draw: `+0x5C pStatListEx`, `+0x60 pInventory`, `+0xE4 pPrevUnit` (prev in the unit-type list), `+0xE8 pPrevUnitInRoom` ("previous unit in the current room").

## 2. Arm A — the payload structs

- **PlayerData**: name + quest/waypoint blocks; the human-facing identity. It's the union member picked when dwType == player.
- **MonsterData** (sizeof 0x60): `+0x00 pMonStats` = the monstats.txt record; `+0x04 Components[16]` = the component ids (HD/TR/LG/...) that drive which cels get drawn.
- **ObjectData**: points to its objects.txt/ObjectMode record — the static-world furniture arm.
- **ItemData** (sizeof 0x74): quality/flags/fingerprint. Items also thread through `pInventory` (Inventory: `+0x08 pOwnerUnit`, `+0x0C pFirstItem`, `+0x14 pInvInfo` equipped-gear list) — i.e. an item's UnitAny is reachable both as a standalone unit and as a node inside another unit's inventory.

These are pure data payloads: they never point back up to rooms. Position lives entirely in Arm B.

## 3. Arm B — Path → Room1 → Room2 → Level → Act (the spatial spine)

This is the chain that matters for rendering, because a unit's *screen* position is derived from its *Path* position projected through the room it sits in.

- **Path** (dynamic, for moving units) carries the fine sub-tile position (dwPosX/dwPosY plus target/offset fields) and a back-pointer to its **Room1**. **StaticPath** (objects/tiles/items) is the lean version: a room pointer + a fixed tile position (no velocity, no targets), because static things never move.
- **Room1** = the collision/active room: its unit list is what the draw walks (units chained via `UnitAny+0xE8 pPrevUnitInRoom`), it holds the CollMap, near-room pointers, and a link up to **Room2**.
- **Room2** = the DRLG/preset room: geometry, presets, near-room graph, and the link up to **Level**.
- **Level** = the area (its levelno indexes Levels.txt), owning its Room2 list and linking up to **Act**.
- **Act** = `pDrlgAct`, reachable directly off the unit at `UnitAny+0x1C` and rooted in the game at `pGame->pDrlgAct[5]` / `pGame->pDrlgRoomList[5]`.

So `UnitAny → pPath → (Room1 → Room2 → Level → Act)` is how any unit answers "which act/level am I in, and where," and the reverse (`Act → Level → Room2 → Room1 → unit list`) is exactly the order the world renderer iterates.

## 4. Arm C — Info → Skill → SkillInfo

`UnitAny+0xA8 pSkills` → **Info** (the per-unit skill controller) → a linked list of **Skill** nodes (level, charges, left/right assignment) → each Skill's **SkillInfo** = the Skills.txt/skilldesc record. This arm is orthogonal to rendering (it drives casting/sequence via `pSeqMode` at `UnitAny+0x30`), included here to complete the hub.

## 5. Ownership & lifecycle

- **Owner**: `pGame` owns everything — the per-act DRLG (`pDrlgAct[5]`), the room lists (`pDrlgRoomList[5]`), and the unit lists. Units do not own their Path/Data payloads' *world*; they borrow into the game-owned room/level/act graph via Arm B. A unit's own `pUnitData`, `pPath`, `pInventory`, `pStatListEx`, and `Info` are allocated with it.
- **Membership**: a unit lives in two intrusive lists simultaneously — the global type/GUID bucket (`pPrevUnit`, hashed by GUID&127) and its current room (`pPrevUnitInRoom`). When a unit crosses a room boundary its Path's Room1 pointer is re-linked and it moves room lists; that re-link is the seam the renderer sees.
- **Determinism**: `dwInitSeed` (unit) + `GameSeed` (game) make spawns reproducible.
- **Client/server twins**: GUID ties a client `UnitAny` to its server one; several hub fields (`+0x64`, `+0x80`, `+0xD8`) are overloaded client-vs-server.

## 6. How this all lands in OUR render work

The world draw is a walk of the Arm-B spine, and each unit it reaches is **projected through the render context** — the one struct we've been fighting.

- **The projection**: for any tile/unit, `screenX = ctx+0x04 − ctx+0x24 + tileAbsX` and `screenY = tileAbsY − ctx+0x28 + ctx+0x08` (`struct.D2Client_RenderContext`; matches `D2Client.FloorFlush`). `tileAbsX` comes from the unit's Path/StaticPath room-space position; the two ctx terms are the draw-window origin (`+0x04` draw_left) and the camera scroll (`+0x24` scroll_x).
- **The room iteration is clipped by the ctx draw-window**: `D2Client.DrawRooms` (0x7A9F0) builds its room-iteration RECT from `ctx` draw-window + scroll (`band = [scrollX, scrollX + (R−L)]`) and culls any tile with `tileWorldX > RECT.R`. So `ctx+0x0C` (draw_right) is literally the bound on how far along the Room1/Room2 walk we draw. That is the tile/room draw path the units hang off of.
- **The shift = the projection's origin term** (`finding.terrain_shift_is_offx_ctxL`): opening a side panel writes `offX (0x6FBCC418) = −320` into `ctx+0x04`, so every tile *and* unit derived from the same Path→Room projection slides −320 (1×). scroll_x (`ctx+0x24`) is untouched — the pan is entirely the draw-window origin, not the camera.
- **Why units desync 2×**: floor tiles and units read the *same* 1× projection, yet units land at −640. The extra −320 is applied in the unit pass (PASS2), not in the shared ctx term — consistent with offX being read at 173 sites while the units path re-reads it. The Arm-A/Arm-B split is why: terrain tiles are projected once off the room; unit sprites are re-projected on top with a second offX application.
- **`reframe.one_cause_view_state`**: the shift, the play-area narrowing, and the world-vs-units desync are one thing — opening a panel mutates the world **view-state** held in this render context (offX + draw_right). The clean fix is to render the whole Arm-B walk in the no-panel view-state (offX=0, draw_right=DispW) and composite the panel on top, so terrain and units share one coherent projection again.
- **The remaining black is downstream of all of this** (`finding.clip_is_d2dx_drop_confirmed`): even when we widen `ctx+0x0C` so `DrawRooms` iterates the full room band and D2Glide submits the far tiles (verts reach x≈1264, 338 in [960,1280)), D2DX drops the dead-zone geometry. So the units-struct → render-context chain correctly *produces* the far-right world geometry; the loss is past the D2Glide→D2DX seam, not in the unit/room walk.

**One-line takeaway**: `UnitAny` is a tagged union — Arm A says *what*, Arm B (`pPath → Room1 → Room2 → Level → Act`) says *where*, Arm C says *what it can do*. The renderer walks Arm B (Act→Level→Room2→Room1→unit list) and turns each unit's room-space Path position into pixels via the render-context origin/scroll/draw-window; the panel-open "shift" and "clip" are just that render context's origin (`+0x04`=offX) and right-bound (`+0x0C`) being rewritten, with the residual black living downstream in D2DX.


# The "world" struct graph — synthesis note

Source of truth for every field/offset below: `D:\_SERVER\esoteric-path\tools\dll-work\plugy-src\chaos\Commons\D2UnitStruct.h`. That header uses Yohann's older names; our own patch logs use the canonical names (`reference/EP-assembly-notes/townCheck.asm`: `CheckIfRoomIsTown(Room1* pRoom)`, `GetTownFromAct(Act* pDrlgAct)`, `GetActFromLevel`). Name map: **Room1 = `Room`, Room2 = `RoomEx`, Act = `ActMap`, ActMisc = `ActData`**; Level, PresetUnit unchanged; RoomTile is the list hung at Room2's warp-tile pointer.

## The core insight: two graphs, not one
The world is a **static description graph** (Room2 → Level → ActMisc, seed-deterministic, screen-independent) with a **live instance graph** (Room1) lazily allocated on top of it. UnitAny rides the live graph; the renderer is a third, per-frame layer that never touches either — which is the whole reason the terrain/click bugs are view-state, not world-state.

## The pointer chain (UnitAny → … → Act)
Grounded in real fields:
- **`Unit.path` (+2C, `Path*`)** and `Unit.ptPos` (+38). `Path.ptRoom` (+1C) → the **Room1** the unit occupies. Path also caches the unit's `x`/`y` (+0C/+10) and `mapx`/`mapy` (+02/+06). Unit also caches `ptAct` (+1C) and `ptGame` (+80) directly.
- **Room1 (`Room`)** `ptRoomEx` (+10, `RoomEx*`) → its **Room2**. (Reverse of Room2→Room1.)
- **Room2 (`RoomEx`)** `ptLevel` (+58, `Level*`) → its **Level**.
- **Level** `ptActData` (+1B4, `ActData*`) → **ActMisc**. The canonical "→ Act" edge closes via **`ActData.ptActMap` (+46C)** → the small **Act (`ActMap`)**; ActMisc↔Act is mutual (`ActMap.ptActData` +48).

So the full walk is: `Unit.path → Path.ptRoom(Room1) → Room1.ptRoomEx(Room2) → Room2.ptLevel(Level) → Level.ptActData(ActMisc) → ActMisc.ptActMap(Act)`.

## Ownership & lifecycle
- **Roots live on Game.** `Game.mapAct[5]` (+BC, `ActMap*`) — one Act per act. `ActData.ptGame` (+9C) points back up; `ActMap.ptFirstRoom` (+10) and `ActMap.ptActData` (+48) fan down. `ActData` owns the DRLG seeds (`seed1`/`seed2` +0/+4), difficulty (+450), room count (`nbRooms` +8), and the special tomb-level fields (`nTalRashaTombLevel` +94, `nBossMummyTombLevel` +484).
- **Level is the per-area container**, chained by `ptNextLevel` (+1AC) into the act's level list; it carries `levelNo` (+1D0, the levels.txt Id), `levelType` (+1C0), its own `seed1/2` (+1C4/+1C8), and bounds `posX/Y/sizeX/Y` (+1C…+28). `Level.ptFirstRoomEx` (+10) heads the level's **Room2** list.
- **Room2 = persistent description.** It exists for the level's whole lifetime, chained by `ptNextRoomEx` (+24); near-graph in `roomExNear` (+08) / `roomsNearCount` (+2C); layout in `posX/Y/sizeX/Y` (+34…+40, room/tile units); `presetType` (+48); `roomFlags` (+28).
- **Room1 = transient instance.** `Room2.ptRoom` (+30) is **NULL until the room is activated**; when a room comes into play a Room1 is allocated and back-linked (`Room1.ptRoomEx`). Room1 holds the runtime-only state: `coll` (+20, collision map), the room's unit list head `ptFirstUnit` (+74, walked via `Unit.ptNextUnitInRoom` +E8), the active near-set `ptNearRooms`/`nbNearRooms` (+00/+24), collision-space bounds `startX/Y/sizeX/Y` (+4C…+58), and `ptNextRoom` (+7C) chaining the currently-active Room1s. Room1s are created/destroyed as units move; Room2s are not. That Room2-is-data / Room1-is-live split is the lifecycle in one sentence.

## What hangs off Room2: PresetUnit and RoomTile
- **PresetUnit** — the fixed seeded contents of a room (chests, waypoints, superuniques, preset monsters). Each carries `unitType` (+0), `txtFileNo` (+4), room-local `xPos`/`yPos` (+C/+10), and chains via `pNext` (+1C). The list hangs off Room2 (canonical `Room2.pPreset`; note this repo's `RoomEx` header truncates at `ptLevel` +58 and does not expand the preset field — the PresetUnit struct itself is defined and verified).
- **RoomTile** — the per-Room2 warp-tile list at **`RoomEx.ptWarpTiles` (+4C)** (typed `void*` in this header). Each RoomTile links a spot in the room to a level transition; this is the structural basis of the warp/adjacency graph that `Vis/Warp` in levels.txt feeds (see `kb/08-levels-maps.md`).

## Connection to our render work
The renderer consumes the **live** graph only and lays a view-state on top:
- **Tile/room draw path.** D2Client walks the active **Room1** list and, per room, emits its terrain tiles plus its unit list (`ptFirstUnit`). World positions are Room1 subtile coords (`startX/Y`, `sizeX/Y`); nothing screen-relative is stored on any world struct.
- **The render context** (`struct.D2Client_RenderContext`) is where world→screen happens: it holds `offX` and the draw-window/clip rect. The concrete knobs we've located are the resolution globals in `kb/15-EP-assembly-notes-index.md` (`GeneralDisplayWidth [6FB8BC48]`, `GeneralDisplayHeight [6FB8BC4C]`) and the tile-extent edits (`!Definite.asm` `ADD ECX,-2F → 50`, `[[6F865B04]] = screen height`; `TileRendering.asm`, `viewmatrix110.asm`).
- **Terrain shift = offX, not world.** Because Room1 tile coords are fixed and seed-deterministic, any apparent terrain movement is the render context's `offX`/`ctxL` draw-window left edge changing (`finding.terrain_shift_is_offx_ctxL`) — i.e. the view-state, not the room graph.
- **One cause = view-state** (`reframe.one_cause_view_state`). The terrain-shift and the bottom-bar click-miss are the same defect: the world struct graph is intact and screen-independent, while the per-frame view-state (render context draw-window + D2Client's panel/resolution geometry) is what moved after the SGD2FreeRes provider swap. The click side is the input twin of the draw side (`clickThrough.txt`: `6FAD81F5` JNZ/JMP gate).
- **Clip is a d2dx drop, not engine culling** (`finding.clip_is_d2dx_drop_confirmed`). The engine's room enumeration (Room1 near-list) correctly yields the edge rooms/tiles; the missing band is dropped downstream by the d2dx/SGD2FreeRes tile culling at the wrapper, confirmed in `kb/14-rendering-stack.md` (the 47px void region + motion-prediction tile culling). So edge loss is renderer-side clip, cleanly separable from the world structs above it.

## Punchline
Room2/Level/ActMisc/Act are a seed-deterministic, screen-agnostic description; Room1 is the live overlay the renderer and units actually touch; and every "the world moved" symptom we've chased lives one layer higher still — in the render context's view-state — never in these structs.


# stats_inv — synthesis note (1.13c)

Everything here hangs off one root: the player `UnitAny`. Two independent pointer graphs grow from it, and they only reconverge at the screen:

- **WHERE the unit is** → `pPath → Room1 → Room2 → Level → Act` (feeds the world tile/room draw).
- **WHAT the unit is** → `pStats → StatListEx → Stat[]` and `pInventory → Inventory → item UnitAny list` (feeds the inventory/character *panel* draw).

The six `stats_inv` structs are the second graph. `InventoryLayout` is the odd one out — it is not in the unit graph at all; it is the static screen-space stencil the panel draw borrows.

---

## 1. The stat chain

```
UnitAny
  +0x5C  pStats  ── StatListEx  (the unit's ONE aggregated/"full" list)
                       Stats(StatVector) : pStat @+0x24 → Stat[ ]  ,  wCount @+0x28
                       fpStatExpires @+0x38   (contributor-removed callback)
                       pNext / pMyLastList ── chain of contributor StatList nodes
```

- **`Stat`** — one record, 8 bytes: `wSubIndex`(+0x00, WORD) · `wStatIndex`(+0x02, WORD = the ItemStatCost id) · `dwStatValue`(+0x04, DWORD). The corpus lists this verbatim: `iStatIndex[0]`/`iStatID[0]`/`iStatValue[0]` at 0/2/4, array packed 8-byte stride, kept sorted by id. `wSubIndex` is why one id can appear twice (e.g. per-skill / per-class stats).
- **`StatEx`** — the same `{wSubIndex, wStatIndex, dwStatValue}` triple as it lives in the *extended* vector, carrying the extra bookkeeping the "Ex" (full) list needs to know **which contributor list a value came from** so it can be re-folded or expired. It is the element type of the aggregated list; `Stat` is the element type of a plain modifier list.
- **`StatList`** — the base node/header: its `Stats` StatVector (`pStat`@+0x24, `wCount`@+0x28 — corpus-grounded) points at the `Stat[]` this node contributes, plus a `pNext` link. A single state (aura, curse, buff) or a single equipped item owns exactly one of these.
- **`StatListEx`** — the unit-owned superset that `UnitAny.pStats` actually points at. It embeds the base `StatList` and adds the multi-layer value vectors, the `pUnit` back-pointer, and `fpStatExpires`(+0x38, corpus-grounded) — the callback invoked when a contributor list is torn off. The `+0x58` field the corpus flags "inside StatListEx" belongs to this extra region.

**Fold/expire lifecycle (the ownership story):** each equipped item and each active state produces its *own* `StatList` of raw modifiers, links it into the owner's `StatListEx` chain via `pNext`, and its values get summed into the aggregated `StatEx[]`. When the item is removed or the state ends, the node is unlinked and `fpStatExpires` recomputes the full list. `ITEMS_UpdateTransferredProperties` (corpus, `D2Game.0x6FC424E0`, args `bUpdateStatList`) is the server-side driver of exactly this. So: the unit **owns** one `StatListEx`; every contributor is **owned by its source** and has list-scoped lifetime, not unit-scoped.

---

## 2. The inventory chain

```
UnitAny
  +0x60  pInventory ── Inventory
                          dwSignature @+0x00   (0x1020304 sanity tag)
                          pOwner       ── back to this UnitAny  (cycle)
                          pFirstItem ─► item UnitAny ─► ... ─► pLastItem   (equipped/stored items)
                          pCursorItem  (item on the mouse)
```

- **`Inventory`** — the runtime container. `pOwner` closes the cycle back to the unit; `pFirstItem…pLastItem` is the linked list of item `UnitAny`s. Each item is a full `UnitAny` with **its own `pStats` (StatListEx)** — that is the physical link between the two structs: an item's stat list is a contributor that folds into the wearer's aggregated list (section 1). Corpus D2Common sigs (`D2Common_FindStackTarget(Inventory*, ...)`, `InventoryHasRoomForItem(D2InventoryStrc*, ...)`) operate on exactly this node.
- **`InventoryLayout`** — **not reached by any pointer from `Inventory`.** It is the static per-panel/per-class grid descriptor: pixel bounds `Left/Right/Top/Bottom` + `SlotWidth/SlotHeight` (cells) + per-cell pixel size. Lifetime = process (a data table), not the unit. The inventory-panel draw (`D2Client.0x3EAC0` in the ref; `UI_INVENTORY_DrawItemBackground(D2InventoryGridInfoStrc*, nX, nY, nItemWidth, nItemHeight, …)` in the corpus) *reads* it to place each `Inventory` item's cell. So `Inventory` says *which items and where in grid-cells*; `InventoryLayout` says *where those cells land in screen pixels*.

---

## 3. Why this sits parallel to the position chain

The map chain `UnitAny.pPath → Path.pRoom1 → Room1.pRoom2 → Room2.pLevel → Level → Act` is the *other* graph. It is what `DrawRooms` (0x7A9F0) walks: it iterates the `Room1` linked list (`Room1.pNext` @+0x74, per-room units via `UnitAny` room-next @+0xE8 — both corpus-grounded) and projects each tile through the render context. The stat/inv graph never touches tiles; it drives the panel. **Same root, disjoint bodies, they only collide in screen space.**

---

## 4. Where it meets our render work

The collision point is the render context — [[struct.D2Client_RenderContext]] (`*0x6FBCC3C4`), which *is* the world view-state. Tile projection is `screenX = ctxL(+0x04) − scrollX(+0x24) + tileAbsX`.

- **Opening the inventory is the trigger for both graphs at once.** The panel it opens is drawn from graph #2 (`pInventory` items placed via `InventoryLayout.Left/Right/Top/Bottom`). That panel occupies the right band — and to make room, the panel-open handler rewrites graph-#1's view-state: `offX → ctx+0x04 = −320` and narrows `ctx+0x0C` toward 960.
- The −320 world shift is [[finding.terrain_shift_is_offx_ctxL]]: the *whole* floor pan is `offX(0x6FBCC418) → ctx+0x04`, `scrollX` untouched. So `InventoryLayout`'s `Left` edge (where the panel starts, screen-space) is conceptually the same X-coordinate the world view is being shoved away from — the panel's static geometry and the world's dynamic `ctxL`/`draw_right` are fighting over one screen column.
- This is the whole point of [[reframe.one_cause_view_state]]: the dead zone, the camera shift, and the world/units desync are **one cause** — opening the panel changes the world view-state. The clean target is to render graph #1 in the *no-panel* view-state (offX = 0, full `ctx+0x0C`) and composite the graph-#2 panel (its `InventoryLayout` region) on top, so the two graphs stop colliding.
- Caveat we proved: neutralizing the ctx window is necessary but not sufficient for the black band — [[finding.clip_is_d2dx_drop_confirmed]]. Once the world is un-shifted, valid floor geometry reaches `x≥640/960` at the D2Glide→D2DX seam (submit max 1264–1296, 338 verts in the dead zone) and is still dropped **downstream in D2DX**, not by `ctx+0x0C`. So the `stats_inv`/panel side dictates *where* the world must not draw; the D2DX target extent dictates *whether* it can.

**One-line takeaway:** `pStats`/`pInventory` and `pPath` are twin graphs off `UnitAny`; the inventory panel is drawn from the former via the static `InventoryLayout` stencil, and the act of opening it is precisely what rewrites the render-context view-state that our terrain-shift and dead-zone work is chasing.

---

### Offsets: sourcing
- Corpus/repo-grounded: `UnitAny.pStats` +0x5C; `StatList.Stats` pStat +0x24 / wCount +0x28; `StatListEx.fpStatExpires` +0x38; `Room1.pNext` +0x74; unit room-next +0xE8; `Inventory` sigs via `D2InventoryStrc`.
- Canonical D2Structs.h (not re-measured vs the 1.13c binary here): `UnitAny.pInventory` +0x60; `Inventory.dwSignature/pOwner/pFirstItem/pLastItem/pCursorItem`; `InventoryLayout.Left/Right/Top/Bottom + SlotWidth/SlotHeight`. Verify against the live binary before hooking.


# gfx_ui structs — how they connect (1.13c RE reference)

**One-line thesis:** the eight gfx_ui structs are *content* (what a sprite/tile/overlay looks like and where it wants to sit in world space). None of them owns the world→screen transform — that lives entirely in the render context. So a panel-open never changes a single field in these structs; it changes `D2Client_RenderContext`, and every one of these structs re-projects through the new view-state. That's why the shift/clip fix belongs at the view-state, not here.

## Confidence tiers (hostile-reader honesty)
- **Project-verified (exact offsets, from our grimoire/1.13c binary):** `D2Client_RenderContext` layout, the FloorFlush/DrawRooms projection, the automap fns (`NewAutomapCell` 0x703C0, `AddAutomapCell` 0x71EA0, `InitAutomapLayer_I` 0x733D0, `DrawAutomapCell` D2GFX 0x7C80, `AutomapLayer*` var 0x11CF28), and the "Lit" cel path.
- **Canonical D2Structs.h field names (community-standard, and our own d2-re-reference §3 states "layouts are stable across versions"):** the sprite triad, automap pair, `Control`, `Waypoint`, `Light`. Field *names* below are dependable; where a byte offset for these isn't in our verified set I name the field and flag it, rather than invent an offset.

---

## 1. The sprite triad — GfxInfo ⊃ GfxCell, driven by CellContext

Ownership is a strict containment + a transient pointer-in:

```
GfxInfo (loaded DC6/DCC file)              CellContext (per-draw state)
  dwVersion / eFormat                        nCellNo   ── frame index ──┐
  numdirs, numcells                          pCellFile ─────────────────┼──► GfxInfo
  cells[] ──► GfxCell*  (per dir × frame)     dwPosX, dwPosY            │
                    │                                                    │
                    ▼                              GfxInfo.cells[nCellNo]│
              GfxCell (one frame)  ◄───────────────────────────────────┘
                flags, width, height
                xoffs, yoffs   (sprite-local anchor, NOT screen)
                lpParent  ──► back to owning GfxInfo
                length, cols[]  (RLE pixel data)
```

- **GfxInfo owns GfxCell.** `GfxInfo.numcells` sizes the `cells[]` table; each `GfxCell.lpParent` points back to its `GfxInfo`. GfxCells have **no independent lifecycle** — they live and die with the file. Lifetime = file load → cache eviction.
- **CellContext owns nothing; it *selects*.** `pCellFile` → a `GfxInfo`; `nCellNo` indexes `GfxInfo.cells[]` to pick the `GfxCell`; `dwPosX/dwPosY` say where to blit it. It is the per-draw cursor, rebuilt every frame.
- **Key point for us:** `GfxCell.xoffs/yoffs` are *sprite-local* anchors, and `CellContext.dwPosX/dwPosY` are already *screen* coords. The world→screen step that produced those screen coords happened earlier, in the render context (§4). The sprite structs are position-agnostic — hand the same `GfxCell` a different `dwPosX` and it draws shifted, unchanged.

## 2. Where the tile/floor GfxCells come from — the world hierarchy

The classic chain feeds the tile draw path. Named by field:

```
UnitAny.pPath ─► Path.pRoom1 ─► Room1.pRoom2 ─► Room2.pLevel ─► Level.pMisc ─► ActMisc.pAct ─► Act
                                   │                 │
              Room1.pRoomNext (active-room list)     Level.pRoom2First (all rooms in level)
              Room1.pUnitFirst (units in room)       Room2.pRoomTiles (tile/gfx data)
              Room1.Coll (collision)
```

- **DrawRooms** seeds off the player's room (`GetRoomFromUnit` #10331 → the `Room1` the player's `Path.pRoom1` points at), then iterates the `Room1` list. Per room it runs the per-tile dispatch (tile struct 0x30 bytes; flags `[t+0x14]` bit-test `0x1C000` = DRAW_GROUND|FLOOR|ROOF; orientation `[t+0x1c]`). Each drawn tile resolves to a `GfxCell` and is handed to the cel-blit thunks (§4).
- **Room1 vs Room2 = lifecycle boundary.** `Room2` is the persistent level-geometry node (always present via `Level.pRoom2First`); `Room1` is the *hydrated/active* instance (`Room2.pRoom1` non-null only near players) that actually carries collision + live units + the tiles that get drawn. Panel-open changes *which* Room1s survive culling, never the Room1/Room2 structs themselves.

## 3. The three overlay structs — parallel projections, NOT the world path

These are drawn *outside* the render-context tile path, which is exactly why they don't shift/clip with the world:

- **AutoMapLayer / AutoMapCell** — one `AutoMapLayer` per `Level` (`AutoMapLayer.nLayerNo` == level no; global head at var **0x11CF28**). Cells hang off the layer's four buckets `pFloors / pWalls / pObjects / pExtras`, each a sorted tree via `AutoMapCell.pLess / pMore`. Each `AutoMapCell` carries `nCellNo` (frame in the automap sprite) + `xPixel/yPixel`. Built by `RevealAutomapRoom` (0x73160) walking a Room's tiles → `NewAutomapCell` (0x703C0) → `AddAutomapCell` (0x71EA0); drawn by **`DrawAutomapCell` (D2GFX 0x7C80, takes a RECT\* cliprect)**. So the automap is a *second, independent* projection of the same Room geometry — its own coord space, its own clip. It stays whole with a panel open because it never touches `D2Client_RenderContext`.
- **Light** — dynamic light entries associated with the active `Room1`/`Act` environment. The connection worth grounding: our live cel handler is **`pfTileDrawLit`** (D2Glide dispatch `+0x9c`, `LiveCelHandler` 0x72F0) — the *lit* tile blit. Lights modulate the same `GfxCell` pixels as they cross that handler. (Light's finer field layout is the thinnest of the eight in our verified set — treat its role, not specific offsets, as load-bearing until read off the binary.)
- **Control** — the UI-widget chain (`Control.pNext` linked list; `dwType`, `dwPosX/dwPosY`, `dwSizeX/dwSizeY`, plus text/edit fields). Controls are the panel's buttons/boxes, drawn by the **compositor/present** stage of `DrawGame` (body: PASS1 0x44302 → tiles → PASS2 0x4436F → compositor 0x4437B → present #10040), i.e. *after and over* the world pass — they sit on top of the dead zone, not in it.
- **Waypoint** — `Waypoint.flags` (one byte per waypoint, arrayed). Pure UI/save state surfaced by the **waypoint side panel**, which is one of the panels whose *opening* is the trigger event: `PanelOpenArray` waypoint slot `+0x50`. Opening it fires the exact view-state write that shifts/clips the world (§4). So `Waypoint` is upstream cause-by-association, not a rendered-world participant.

---

## 4. Tie-in to our render work — the whole point

Everything above pours through **one** transform, the render context.

**`D2Client_RenderContext`** (`struct.D2Client_RenderContext`, instance `*0x6FBCC3C4`) IS the world view-state:
- `+0x04 draw_left` == **offX** (0 no-panel, **−320** panel-open) — the shift
- `+0x0C draw_right` — iteration bound (1280 no-panel, **960** panel-open) — the issue-side clip
- `+0x24/+0x28 scroll_x/scroll_y` — camera origin (**not** touched by panels)

The tile projection (FloorFlush / DrawRooms band) is literally:
```
screenX = ctx.draw_left(+0x04) − ctx.scroll_x(+0x24) + tileAbsX
```
That `screenX` is what becomes a tile's `CellContext.dwPosX`. So:

- **`finding.terrain_shift_is_offx_ctxL`** — panel-open writes `offX 0→−320` → `ctx+0x04 0→−320`; `scroll_x` untouched. The floor/terrain GfxCells didn't change; their **CellContext.dwPosX moved −320** because the shared `ctx+0x04` term moved. Confirmed exactly −320 (=−DispW/4), standing still.
- **`reframe.one_cause_view_state`** — shift (`+0x04`), clip (`+0x0C` narrow), and the world/units desync are ONE cause: the panel rewrites the view-state. The gfx_ui structs are all downstream of it, so the clean fix authors a coherent no-panel view-state for the whole world draw — you do **not** patch per-struct or per-sprite, and you keep `ScreenOpenMode` live for UI/culling. (Units pick up an *extra* −320 beyond the shared 1x tile projection — a separate PASS2/unit-layer source, not the ctx.)
- **`finding.clip_is_d2dx_drop_confirmed`** — this exonerates the gfx-struct layer as the blackout cause. With the view-state widened, the tile GfxCells *do* reach the D2Glide→D2DX seam in the dead zone (submit max 1264–1296, 338 verts in [960,1280)) and still render **black**. The geometry — valid GfxCell frames, same path as the [640,960] tiles that paint fine — is dropped at/after D2DX. So: `ctx+0x0C` governs which GfxCells get **issued/projected**; **D2DX** governs whether the issued verts get **painted**. Two distinct clip layers; neither is a property of the eight structs.

### The stack, one line
```
GfxInfo.cells[nCellNo] ─► GfxCell  (content, position-agnostic)
        │  positioned by
        ▼
CellContext.dwPosX/Y  =  D2Client_RenderContext (+0x04 offX, −0x24 scroll, +tileAbsX)   ← the shift & issue-clip live HERE
        │  blitted via
        ▼
CelBlitThunk → D2gfx #10001 → D2Glide pfTileDrawLit (Light modulates) → grDrawVertexArrayContiguous → D2DX  ← the terminal paint-clip lives HERE
```
Automap (`AutoMapLayer/Cell` → `DrawAutomapCell`), `Control` (compositor), and `Waypoint` (panel state) all sit *off* this spine — which is exactly why they never shift or clip when the world does.

**Bottom line for the fix:** don't touch the content structs. Author the no-panel view-state in `D2Client_RenderContext` (neutralize `offX`→`+0x04`, restore `+0x0C`→DispW) AND defeat the D2DX dead-zone drop. Both are view/pipeline layers; the eight gfx_ui structs are innocent bystanders that simply render wherever the view-state tells them to.

Source files: `E:\Diablo 2 - Esoteric Path\esoteric-dll\grimoire\symbols.jsonl` (render ctx, cel path, D2DX findings), `...\research\kb\reference\d2-re-reference-1.13c.md` (automap fns, struct-stability note, tile params), `...\research\kb\reference\panelworld-mods-and-hooks.md` (room/tile draw chain).


ATTEMPT I (v1.12.0, council wave-5 recipe): author the NO-PANEL world lens each frame before PASS1 -- in the existing widen hook (0x6FAF4302), when a panel is open set offX(0x6FBCC418)=0, ctx+0x04(draw_left)=0, ctx+0x0C(draw_right)=DispW, dirty|=1; ScreenOpenMode left TRUE (culling+UI live). Grounded: the panel handler 0x6FB5AEC0 writes ONLY offX+mode from DisplayWidth (no separate wall global), so this run also DIAGNOSES OQ.walls_shift_source. OUTCOMES: (a) floor+walls+units all un-shift (character/waypoint bisected by panel) => walls' source IS the ctx, blocker closed; (b) floor+units un-shift but walls stay => walls recompute in PASS2 (0x6FB3B110) => name that next; (c) clicks coherent? (offX=0 persistent should self-solve per m1/m2); (d) x>=640 goes black => D2DX SurfaceIdTracker retag confirmed = the 2nd knob (m2).  ==> RESULT (v1.12.0, WIN): [NOSHIFT] fired once (offX -320->0, ctxL -320->0, ctxR->1280) and HELD every frame (offX 0->0). The WORLD UN-SHIFTED -- character/waypoint bisected by the panel, walls came with it, clicks fine. Geometry reaches the dead zone (probe band[960,1280)=66-104, TILEBAND max=1277). TWO residuals remain: (1) the dead zone is still BLACK = the D2DX retag (m2's 2nd knob); (2) 1-2 FLOOR sub-layers still shift (a source our ctx authoring doesn't cover). The multi-source shift is otherwise SOLVED by authoring the no-panel lens each frame.


v1.12.0: after authoring the no-panel lens (offX=0/ctxL=0/ctxR=DispW each frame), the WORLD, WALLS and UNITS un-shift correctly -- but 1-2 FLOOR sub-layers still sit at the shifted position. So a floor sub-layer projects through a source our per-frame ctx authoring does NOT cover (a cached projection, a pre-PASS1 draw, or an offX re-read in the tilesub/floor-sublayer path 0x6FAF4307). Minor visual residual. Find via which draw path emits the lagging layer. Polish -- lower priority than the black.  ==> IDENTIFIED (v1.12.2): it's the play-area rect playAreaW (0x6FBA9E14) -- the floor draws into it AND the hit-test reads it; narrowed to ~640 on panel-open (EP's 2022 HOM/4-tile-buffer notes). Authoring playAreaW=DispW/frame is the test.  ==> v1.12.3: nViewXOffset was ALREADY 0 at our pre-PASS1 hook (no-op) -> the floor shift is COMPUTED INSIDE PASS1, not a pre-authorable global. See [[finding.floor_shift_internal_to_pass1]]. Council consult queued.  ==> v1.12.6 [PROBE glidevtx count=0 @0xBEFC]: 0xBEFC is NOT the floor's live submit path. The floor's actual grDraw site is unidentified. See [[finding.floor_shift_ledger]]. Next = unfiltered return-addr logger.


THE LAST BARRIER TO ZERO BLACK: with the world un-shifted, floor geometry reaches x>=640 (probe band[960,1280)=66-104) and D2DX renders it BLACK. Confirmed m2's prediction: D2DX SurfaceIdTracker retags world batches with minx>=gameSize/2(=640) as USER_INTERFACE while ScreenOpenMode&1 -> the UI projection drops the world verts. Fix (council m2): runtime hot-patch the minx>=640 retag compare in glide3x during the world draw, restore after. We CANNOT rebuild D2DX. First step: read the D2DX source (bolrog/d2dx SurfaceIdTracker.cpp) to nail whether the retag drops COLOR or only writes metadata (m5 disputed) -> then locate + hot-patch it.  ==> CORRECTED 2026-07-13 (read D2DX source): the surfaceId retag is METADATA ONLY (SetSurfaceId on vertices, for AA/motion) -- it does NOT relocate color (m5/chat was right, m2 wrong). The real clip is D2DX RenderContext::UpdateViewport, which sets the game VIEWPORT + SCISSOR to _gameSize.width (=960 for our desktop). World pixels x in [960,1280] are scissored -> black. FIX = widen _gameSize to 1280 via SetCustomResolution (our feature_d2dx.c / D2DXWiden). It failed alone earlier ONLY because the world was still SHIFTED (geometry never reached [960,1280]); with v1.12.0 no-shift the world fills [0,1280], so no-shift + D2DXWiden TOGETHER should fill the dead zone. RT is >=1024 (max(1024,w)); if a thin [1024,1280] band stays black, the RT alloc is the next tweak.


Esoteric Path (Doug, 2022 notes) + Necrolis + Mir Drualga (phrozen_code Discord)

THE FLOOR + THE MOUSE HIT-TEST are governed by the SAME play-area rect: playAreaW (0x6FBA9E14, RVA 0xF9E14) / playAreaH (0x6FBA9E18), narrowed to ~640 on panel-open. This is the '4-tile buffer'/Hall-of-Mirrors region: the world isn't rendered there by default. SOURCES (all trusted): EP's own d2-re-reference v0.8 note (playAreaW view struct, 'same rect governs mouse hit-test -> the non-clickable black') + our diag.c RD(0xF9E14); phrozen_code Discord 2022-11 -- Necrolis: 'd2dx might be doing it via a clipping rect' (= the RenderContext viewport/scissor we re-found); Mir Drualga (SGD2FreeRes author): 'those black rects are hardcoded in SGD2FreeRes to hide the HOM from not adding assets'; EP: 'a 4 tile buffer... also appears on the left if you open the character stats pane'. FIX (v1.12.2): author playAreaW=DispW each frame -- the bottom-void fix on the horizontal axis. Should make the floor cover the dead zone AND make click-to-move work standing still (currently only works while moving).


THE FLOOR SHIFT IS INTERNAL TO PASS1 (0x6FB3B0C0), not a pre-authorable global. Authoring the no-panel lens each frame BEFORE PASS1 (offX=0, ctxL=0, ctxR=DispW) un-shifts WALLS + UNITS perfectly (they read ctxL) but the FLOOR still shifts. Ruled out, each a NO-OP at our pre-PASS1 hook: playAreaW(0x6FBA9E14)=already 1280; nViewXOffset(D2FloorTileParamsStrc 0x6FB9A9CC, EP's 'terrain shift')=already 0 ([FLOORPARAMS] never fired). Yet the floor is ISSUED full-width (TILEBAND tiles reach screenX 1278, 637 in [960,1280)) -- so it's drawn wide but at the SHIFTED position. The floor-flush projection we RE'd (screenX=ctx+0x04 - ctx+0x24 + tileAbsX) SHOULD use our ctxL=0; since the floor is shifted anyway, the FLOOR uses a different scroll/offset OR its collected tileAbsX carry the shift. Candidate: the DrawGame scratch scroll (0x6FBA9F60/5C) the passes overwrite. Need to hook INSIDE PASS1 or find the floor's internal horizontal source.


THE DISCORD CORPUS: research/kb/corpus/discord/phrozen_code_channel.json = 52,604 messages from PhrozenKeep's #phrozen_code. Top authors (all TRUSTED-tier candidates): Necrolis(6737), Lectem(2883), whist(1930), Kingpin(1495), Firehawk, Silvermane, Ogodei, eezstreet, Mir Drualga(SGD2FreeRes author). DOUG IS IN IT as 'Esoteric Path' (47 msgs, 2022-06 to 2025-06). His 2022-11 thread IS this exact problem: he asked how to fill the 47px bottom void; Necrolis: 'd2dx might be doing it via a clipping rect' (= the RenderContext viewport we re-found); Mir: 'those black rects are hardcoded in SGD2FreeRes to hide the HOM from not adding assets'; EP: 'a 4 tile buffer... also appears on the left if you open the character stats pane'. The bottom void + the side-panel void are the same phenomenon, documented by Doug in 2022. whist = Doug's 'whilst'.


council wave-6 (chat m1, gem m2, clau m4)

CONVERGENCE: the floor's shift is applied at a point our PASS1-call hook (0x6FAF4302) MISSES -- we zero the globals AFTER the floor grabbed the shifted value. Candidates: (m4/clau) the floor SNAPSHOTS the scratch scroll at the DrawGame prologue (0x6FAF42E0 mov[0x6FBA9F60],ecx / 0x6FAF42E6 mov[0x6FBA9F5C],edx from ctx, at the TOP of DrawGame, before our hook) OR bakes the shift into tileAbsX at room COLLECTION (tilesub pre-projects); (m2/gem) the floor's D2Gfx draw-offset (ctx+0x04) is set early via D2Gfx_SetDrawOffset before our hook -> hook HIGHER; (m1/chat) the floor submits the CORRECT unshifted X and D2Glide applies -DispW/4 INTERNALLY via its nScreenPanels arg in the live drawer 0x6F856840 -> force nScreenPanels=0 for the floor path. DECISIVE delta (m1): track one floor tile's submittedX (D2Client) vs vertexX (D2Glide) panel-open vs closed -- clientDelta=-320 => upstream (D2Client); glideDelta=-320 => D2Glide. v1.12.4 tests the scratch-scroll hypothesis (author 0x6FBA9F60/5C = ctx + log the delta). UNIFYING PIN (m4): both the floor AND the click are a value = DispW-320 that our hook doesn't reach; one resolution-differential scan collects EVERY DispW-320 address.


TWO side-answers for later. (1) MENU shrink from SetCustomResolution(1280): no native menu-exclusion; scope it DYNAMICALLY by D2 client state -- hook the game-enter/exit (state 0x01 menu / 0x03 in-game) and toggle SetCustomResolution(800 menu / 1280 in-game), or gate it in the top-level render dispatch. (2) CLICK-into-black-only-while-moving (m4): it's the click-to-move path-VALIDITY / acceptance rect, a separate rect derived from DispW-320, refreshed only by motion (stale when standing still). Fix: widen the acceptance rect (find via the resolution-differential DispW-320 scan); if clicks then register but won't path, force the walkable-region refresh independent of motion.


council wave-6b (chat m10, gem m11, clau m12)

NEW LEADING THEORY (m10, corroborated by m12): the floor/roof shift is a D2DX POST-SUBMIT bug, not a D2Client origin. (1) D2DX SurfaceIdTracker retags world batches with minx>=gameSize.w/2(=640) as USER_INTERFACE when a panel is open. (2) In OnBufferSwap, D2DX applies the player MOTION-PREDICTION offset ONLY to surfaceId!=UI && cat!=Player -- so the UI-mislabeled floor/roof batches SKIP the camera offset while the walls get it -> they slide apart AS the prediction changes with click-to-move = 'follows the mouse'. Isolated batches (the palace doorway) most susceptible. Ruled out upstream: offX held 0, floor still shifts (D2Client camera not the source). m12: the mouse-tracking PROVES live-read (not baked at collection); the black split MIRRORS sides with the panel = a compositing/retag split, not a clip. DEFINITIVE TEST (m10): d2dx.cfg [optouts] nomotionprediction=true -> if the slide stops, confirmed. FIX (m10, keeps motion pred): in the grDrawVertexArrayContiguous hook, temporarily set ScreenOpenMode(0x6FBCC414)=0 for WORLD-TILE calls only (filter by return addr: D2Glide+0xBEFC DrawFloor, +0x67AB/+0x67B9 DrawWall1/2), restore after -> D2DX won't retag the floor as UI. Start with +0xBEFC.


The Esoteric Path (verified) + chat (council)

THE FLOOR AND WALLS SUBMIT FROM DIFFERENT D2Glide CALL SITES (verified via disasm of the call to the grDraw wrapper 0x6F856026): DrawFloor -> 0x6F85BEF7 call (return 0x6F85BEFC = D2Glide+0xBEFC); DrawWall1 -> 0x6F8567A6 (return 0x67AB); DrawWall2 -> 0x6F8567B4 (return 0x67B9). Our glidevtx probe was filtering return-addrs [0x6840..0x6B40] = the WALL calls, so all our floor-drawer RE + the FloorCompX seam-compensation were hitting the WALLS, not the floor -- which is exactly what Doug saw (the correctly-placed tiles moving by +320). CORRECT floor filter = 0xBEFC. chat's public-1.13c-table mapping (DrawWall1/2=0x67AB/0x67B9, DrawFloor=0xBEFC) is CONFIRMED for our build. v1.12.6 + INI: glidevtx arg=0xBEFC, FloorCompX=320 -> compensate the FLOOR at its seam.


The Esoteric Path (Doug directive 2026-07-13)

DOCUMENTATION RIGOR RULE: every recorded observation MUST carry a valid way to confirm it -- the DLL VERSION it was seen in + the exact LOG line (or disasm) that proves it. Mark PROVEN (log/disasm) vs INFERRED (visual). No narrative claims without a verification handle. Applies to all esoteric.dll findings henceforth.


FLOOR+ROOF SHIFT LEDGER (evidence-backed; walls+units are FIXED, floor+roof still shift when a panel opens). Each source RULED OUT with version+log:
* WALLS/UNITS fixed: v1.12.0 authoring no-panel lens (offX=0/ctxL=0/ctxR=DispW) pre-PASS1. PROVEN: [NOSHIFT] log fires each frame + visual (walls/units snap into place).
* offX (0x6FBCC418): RULED OUT v1.12.5. PROVEN: [SHIFT] 'offX 0->0' held every frame, floor STILL shifted.
* playAreaW (0x6FBA9E14): RULED OUT v1.12.2. PROVEN: [NOSHIFT] 'playAreaW 1280->1280' (already full, no-op).
* nViewXOffset (D2FloorTileParamsStrc 0x6FB9A9CC): RULED OUT v1.12.3. PROVEN: [FLOORPARAMS] NEVER logged (condition fvx!=0 false -> already 0 at our hook).
* scratch scroll (0x6FBA9F60/5C): RULED OUT v1.12.4. PROVEN: [SCRATCH] NEVER logged (already == ctx scroll).
* D2Glide drawer 0x856840 vertex math: RULED OUT by DISASM -- vertexX = baseX + subtileX; the 640 midpoint feeds ONLY the cull bounds ([esp+0x30]/[esp+0x38], already NOP'd), no -DispW/4 on the vertex.
* D2DX motion prediction: RULED OUT -- d2dx.cfg nomotionprediction=true -> no change (Doug); + the shift is STATIC not dynamic (Doug correction), which motion-prediction wouldn't produce.
* grDraw site 0xBEFC (chat's 'DrawFloor'): RULED OUT v1.12.6. PROVEN: [PROBE glidevtx] arg=0xBEFC -> count=0 (0 hits). 0x6F85BEF7 IS a call site on disk but is NOT on the live floor path in our build.
VERIFIED-live: WALLS submit via grDraw returns 0x67AB/0x67B9 (disasm: 0x6F8567A6/B4 call the wrapper 0x856026; probe with arg=0x6840 fired in v1.11-1.12.5). 0x6840==walls is INFERRED (Doug: compensating that filter moved the CORRECTLY-placed tiles) + chat labels them DrawWall1/2.
OPEN: the FLOOR's actual LIVE grDraw return address is UNIDENTIFIED (not 0x6840=walls, not 0xBEFC=count0). RIGOROUS NEXT: an UNFILTERED return-address logger (probe arg=0) that dumps every distinct grDraw return addr + its vertex-X range + count -> identify the floor by its signature, THEN compensate/fix that exact site.


CONVERGENCE: measure the floor batch's vertex X at the grDraw seam, panel-CLOSED vs panel-OPEN. dMinX=-320 => D2Client submits the floor shifted (UPSTREAM); dMinX=0 => D2DX shifts it (DOWNSTREAM). Also capture the caller return-addr to NAME the floor's live site (our 0xBEFC guess fired 0x). chat: D2DX's 0xBEFC 'DrawFloor' is only an IdentifyGameAddress label, not the live path; forward is unchanged (OnDrawVertexArrayContiguous copies x/y verbatim). gem: if downstream, read D2DX's VS projection matrix via a D3D11 VSSetConstantBuffers vtable hook (matrix[12]=X translation). clau: DISSENT-then-agree -- 'mouse kills it' argument uses the RETRACTED mouse-tracking premise (Doug corrected to STATIC), so its mouse-sweep isn't our discriminator; BUT clau's mouse-INDEPENDENT argument is strong: walls render correct through the SAME D2DX pipeline, so if D2DX shifted world batches walls would shift too -> predicts dMinX=-320 (client-side). clau CAUTION taken: do NOT do the ScreenOpenMode=0 mask (attempt-A anti-pattern: fixes readers who look, misses others, desyncs hit-test). IMPLEMENTED v1.12.7 [Probes] Survey=1 + glidevtx arg=0 -> [SURVEY] rows per caller, closed vs open.


THE LIVE grDraw CALLERS (v1.12.8 Survey, panel-CLOSED only so far): D2Glide+0x6A55 (n=87710, x=-32..1296, the dense one), 0xA344 (n=16767, x=0..1151), 0xA555 (n=5340, x=303..1152), 0x704A (n=895, x=1216..1296), 0x9AF4 (n=293, x=11..788). CORRECTION (rigor): finding.floor_vs_wall_grdraw_sites claimed walls=0x67AB/0x67B9 (from disasm of call sites 0x6F8567A6/B4) -- but those return-addrs DO NOT APPEAR in the survey, so they do NOT fire on the live path. The disasm found call sites that aren't reached. The REAL callers are the 5 above. Still OPEN: which is the FLOOR (need panel-OPEN data -> the caller with dMinX~-320). v1.12.8's run was the menu run (open[n=0] everywhere); a park+toggle run is needed. NOTE the survey only counts IN-GAME frames (eso_probe_frame runs from the DrawGame hook, not at the menu).


The Esoteric Path (v1.12.9 [D2DXWiden] log)

MENU FIX. v1.12.9 grSstWinOpen log: the IN-GAME open = res=4099(0x1003) with worldCtx=0x214439AC (NON-NULL); the MENU opens = res=7 and res=4097 with worldCtx=NULL. The world render ctx (*0x6FBCC3C4) is set at the in-game open and null at the menu -> gate is worldCtx!=null. FIX v1.13.0: eso_d2dx_set_wide calls SetCustomResolution(1280) only when worldCtx!=null -> menu keeps native size, in-game world stays widened. (grSstWinOpen arg1=window handle; the resolution is arg2=[esp+0x2C].)


The Esoteric Path (v1.12.9 [SURVEY] open data)

SURPRISE: the floor shift is likely DOWNSTREAM in D2DX, not at the D2Client->grDraw submit. Survey open-vs-closed, 5 live callers: 0x6A55 (floor, n=828558, closed x=-32..1311 / open x=-32..1296 => dMinX=0, dMaxX~-15: STABLE, NOT shifted at submit); 0x09AF4 (dead-zone FILL, open x=640..1280 n=22082 = the world we enabled behind the panel, WORKING); 0xA555 (dMinX=-84); 0xA344 (dMinX=33); 0x704A (dMinX=16). NO CALLER submits shifted by -320. So the floor is submitted un-shifted and D2DX shifts it downstream => chat/gem's branch, AGAINST clau's client-side prediction (reconciled: D2DX shifts only floor-category batches, not walls). CAVEAT: aggregate min/max is confounded by the changing tile set on open, but the ABSENCE of any -320 across all 5 callers is robust. nomotionprediction=true gave no change, so it's a D2DX transform OTHER than motion-prediction. NEXT: specific-tile track OR gem's D3D11 VSSetConstantBuffers matrix read.


mir (Diablo-II-Address-Table, public domain)

Horizontal screen shift (viewport pan offset). Named exactly in FOCUS. | src: D2Client.dll	ScreenShiftX	Offset	0x11B9A0


Vertical screen shift (viewport pan offset). Named exactly in FOCUS. | src: D2Client.dll	ScreenShiftY	Offset	0x11B9A4


General display height. Named exactly in FOCUS. | src: D2Client.dll	GeneralDisplayHeight	Offset	0xDBC4C


Automap open flag. | src: D2Client.dll	IsAutomapOpen	Offset	0xFADA8


Game menu open flag. | src: D2Client.dll	IsGameMenuOpen	Offset	0xFADA4


Help screen open flag. | src: D2Client.dll	IsHelpScreenOpen	Offset	0xFAE04


Inventory arrange mode (UI panel layout state). | src: D2Client.dll	InventoryArrangeMode	Offset	0x11B99C


New-skill button pressed flag (UI element state). | src: D2Client.dll	IsNewSkillButtonPressed	Offset	0x11C30C


New-stats button pressed flag (UI element state). | src: D2Client.dll	IsNewStatsButtonPressed	Offset	0x11C308


In-game mouse cursor X (screen coordinate). | src: D2Client.dll	IngameMousePositionX	Offset	0x11B828


In-game mouse cursor Y (screen coordinate). | src: D2Client.dll	IngameMousePositionY	Offset	0x11B824


Draws centered unicode text on screen. | src: D2Client.dll	DrawCenteredUnicodeText	Offset	0xBC4E0


Render resolution mode selector. Comment gives enum values. | src: D2GFX.dll	ResolutionMode	Offset	0x11260	0 = 640x480, 1 = Main Menu, 2 = 800x600, 3 = 1344x700


Active video/render backend selector. Comment gives enum values. | src: D2GFX.dll	VideoMode	Offset	0x11258	1 = GDI, 2 = Software, 3 = DirectDraw, 4 = Glide, 5 = OpenGL, 6 = Direct3D, 7 = Rave


Windowed vs fullscreen flag. | src: D2GFX.dll	IsWindowedMode	Offset	0x1125C


HWND of the render window. | src: D2GFX.dll	WindowHandle	Offset	0x11264	gtGfxSystem.hWnd


Draws a CEL (sprite) via graphics context — core sprite/cell blit entry point. | src: D2GFX.dll	DrawCelContext	Ordinal	10041


Draws a rectangle primitive. Named in FOCUS. | src: D2GFX.dll	DrawRectangle	Ordinal	10014


DirectDraw backend display width. | src: D2DDraw.dll	DisplayWidth	Offset	0x101D4


DirectDraw backend display height. | src: D2DDraw.dll	DisplayHeight	Offset	0x101CC


DirectDraw bit-block (blit) width. | src: D2DDraw.dll	BitBlockWidth	Offset	0x101C8


DirectDraw bit-block (blit) height. | src: D2DDraw.dll	BitBlockHeight	Offset	0x101D8


DirectDraw CEL display left bound. | src: D2DDraw.dll	CelDisplayLeft	Offset	0x101E8


DirectDraw CEL display right bound. | src: D2DDraw.dll	CelDisplayRight	Offset	0x101EC


GDI backend bit-block (blit) width. | src: D2GDI.dll	BitBlockWidth	Offset	0xCA80


GDI backend bit-block (blit) height. | src: D2GDI.dll	BitBlockHeight	Offset	0xCA90


GDI CEL display left bound. | src: D2GDI.dll	CelDisplayLeft	Offset	0xCA98


GDI CEL display right bound. | src: D2GDI.dll	CelDisplayRight	Offset	0xCA9C


Direct3D backend display width. | src: D2Direct3D.dll	DisplayWidth	Offset	0x1AB44


Direct3D backend display height. | src: D2Direct3D.dll	DisplayHeight	Offset	0x1AFD4


Draws unicode text. | src: D2Win.dll	DrawUnicodeText	Ordinal	10150


Measures popup text width/height (layout). | src: D2Win.dll	GetPopupUnicodeTextWidthAndHeight	Ordinal	10177


Measures drawn text width (layout). | src: D2Win.dll	GetUnicodeTextDrawWidth	Ordinal	10028


Measures drawn text width for N chars (layout). | src: D2Win.dll	GetUnicodeTextNDrawWidth	Ordinal	10055


Sets popup text content. | src: D2Win.dll	SetPopUpUnicodeText	Ordinal	10085


Selects active text font for drawing. | src: D2Win.dll	SetUnicodeTextFont	Ordinal	10184


Loads a CEL sprite file. | src: D2Win.dll	LoadCelFile	Ordinal	10111


Unloads a CEL sprite file. | src: D2Win.dll	UnloadCelFile	Ordinal	10126


Main-menu mouse cursor X (screen coordinate). | src: D2Win.dll	MainMenuMousePositionX	Offset	0x21488


Main-menu mouse cursor Y (screen coordinate). | src: D2Win.dll	MainMenuMousePositionY	Offset	0x2148C


Retrieves a CEL frame from a CEL context (sprite frame access). | src: D2CMP.dll	GetCelFromCelContext	Ordinal	10021


Closes a CEL file (sprite resource cleanup). | src: D2CMP.dll	CloseCelFile	Ordinal	10065


Hack replaces 640x480 mode with 1066x600 true 16:9. 0x280=640, 0x1E0=480, 0x42A=1066, 0x258=600. Note width 1066=0x42A but render uses 1064=0x428 in one spot (see D2gdi Part1). | src: replacing 640x480 mode; height wouldn't change, width would, true 16:9


Resizes the game window on resolution change. Sets screen width [eax] then screen height [ecx]. Left unmodified: game crunches elements, mouse cannot move past 640th x / 480th y, black lines. RENDER/window sizing. | src: ORIG: C7 00 80020000  mov [eax],00000280 (640 screen width); C7 01 E0010000  mov [ecx],000001E0 (480 screen height).  PATCH: C7 00 2A040000 mov [eax],0000042A (1066); C7 01 5802000


Sets values determining how D2 renders the game (width render into esi, height render into edx). Unaltered: stretches 640x480 to fill window then crashes. WIDTH RENDER USES 1064 (0x428) NOT 1066 — author: non-multiple-of-4 width overflows onto LEFT side of screen. Height render 600. | src: ORIG: BE 80020000 mov esi,00000280 (640 game's width render); BE E0010000 mov edx,000001E0 (480 height render). PATCH: BE 28040000 mov esi,00000428 (1064 width render); BE 58020000

width render->0x428(1064), height render->0x258(600)


Sets x-position where D2 should STOP rendering entities (walls, pillars). Unmodified: anything past x=640 not rendered (or incorrectly); no crash. Write target [D2gdi.dll+0xCA9C]. RENDER BOUNDARY. | src: ORIG: C7 05 9CCA876F 80020000  mov [D2gdi.dll+0xCA9C],00000280 (640, x-pos stop rendering). PATCH: C7 05 9CCA876F 2A040000 mov [D2gdi.dll+0xCA9C],0000042A (1066)


THE 0x10E29 BLOCK. Sets D2 field-of-vision, character/map positioning, and UI element positions. mov eax,0x280 then stores eax to [D2client+0xDBC48] (width logic), then mov [D2client+0xDBC4C],0x1E0 (height logic). Unmodified: renders as 640x480 and places game at upper-left of window. The width store (A3 48BCB86F) is left UNMODIFIED; only the eax immediate (640->1066) and the height DWORD (480->600) change. | src: ORIG: B8 80020000 mov eax,00000280 (640); A3 48BCB86F mov [D2client.dll+0xDBC48],eax (width logic); C7 05 4CBCB86F E0010000 mov [D2client.dll+0xDBC4C],000001E0 (480 height logic).


Memory location storing the game's logic height. Written directly as DWORD constant in the 0x10E29 block. | src: C7 05 4CBCB86F 58020000  mov [D2client.dll+0xDBC4C], 00000258 ; 600 height logic


THE 0xC3A11 UI-PANEL OFFSET SITE. Original 640x480-mode panel placement: writes ebx (=0) into panel x-offset [+0x11B9A0] and y-offset [+0x11B9A4]. Governs alignment of quest buttons, waypoint buttons, mercenary stats. reg->mem is fewer bytes than DWORD-const->mem, so 8 extra bytes needed to inject HD constants. | src: 89 1D A0B9BC6F  mov [D2client.dll+0x11B9A0],ebx (x-offset of panels, ebx=0); 89 1D A4B9BC6F  mov [D2client.dll+0x11B9A4],ebx (y-offset of panels, ebx=0)


Memory storing X-offset positioning of UI panels. 640x480 -> 0; 800x600 -> 0x50 (80); 1066x600 -> 0xD5 (213). Written via 89 1D A0B9BC6F (reg) or C7 05 A0B9BC6F (const). | src: stores the x-offset positioning of panels; 1066x600 value 000000D5 (213)


Memory storing Y-offset positioning of UI panels. 640x480 -> 0; 800x600 AND 1066x600 -> 0xFFFFFFC4 (-60). Because 1066x600 shares 800x600's height (600), the y-offset code for 800x600 is reused. | src: stores the y-offset positioning of panels; value FFFFFFC4 (-60)


THE 75 16 PER-RESOLUTION BRANCH. Original: jne to 0xC3A11 (640x480 code) if not 800x600 mode. 800x600 block sets x-offset=0x50(80) and y-offset=0xFFFFFFC4(-60), then EB 0C jmp to 0xC3A1D past the 640x480 code. 640x480 block (at 0xC3A11) writes ebx(0) to both offsets. jne displacement = 0x16. | src: 75 16  jne D2client.dll+0xC3A11 (jump to 640x480 code if not 800x600); C7 05 A0B9BC6F 50000000 mov [+0x11B9A0],00000050 (80 x-offset); C7 05 A4B9BC6F C4FFFFFF mov [+0x11B9A4],FFFFF

jne rel8=0x16 -> target 0xC3A11; 800x600 x=0x50, y=0xFFFFFFC4


PATCHED per-resolution branch: author reorders so the shared y-offset store (-60) runs FIRST for both 800x600 and 1066x600 (both height 600), freeing 6 bytes and cutting needed bytes 8->4. jne displacement CHANGED from 0x16 to 0x0C. Now: write y-offset -60; jne 0xC3A11 (=1066x600 code) if not 800x600; 800x600 sets x=0x50(80); jmp past; the 0xC3A11 '1066x600 code' sets x-offset=0xD5(213); two trailing 90 NOPs (leftover bytes). CONCLUSION ON PANEL SHIFT: panel x-offset 213 for  | src: C7 05 A4B9BC6F C4FFFFFF mov [+0x11B9A4],FFFFFFC4 (-60 y-offset); 75 0C jne D2client.dll+0xC3A11 (jump to 1066x600 code if not 800x600); C7 05 A0B9BC6F 50000000 mov [+0x11B9A0],0000


Author's reasoning verbatim-ish: reg->mem needs fewer bytes than DWORD-const->mem, so HD mode needs 8 extra bytes; instead of call+NOP he reorders. Because height is unchanged 800x600<->1066x600, the 800x600 y-offset setup code is reused, freeing 6 bytes and reducing writes from 8 to 4 bytes. | src: 'the height remains the same when switching from 800x600 mode to 1066x600 mode... By reordering the code, I can reuse the code that sets up the y-offset positioning for 800x600 mod

8 extra bytes needed -> reduced to 4; 6 bytes freed


Glide mode entity render dimensions. Writes rendering width [D2glide.dll+0x15A68] and height [D2glide.dll+0x15B04]. Unchanged: entities rendered at incorrect location. RENDER. | src: ORIG: C7 05 685A866F 80020000 mov [D2glide.dll+0x15A68],00000280 (width 640); C7 05 045B866F E0010000 mov [D2glide.dll+0x15B04],000001E0 (height 480). PATCH: ...2A040000 (1066); ..


Sets D2 (Glide) window size. Loads ptr [glide3x.dll+0x1C9A0] (width addr) into edx then writes 640; loads ptr [glide3x.dll+0x1C82C] (height addr) into ecx then writes 480. NOTE: Cheat-Engine relative addresses differ from hex-editor addresses (offset by 0xA00). In the printed PATCH the loader/immediate opcodes C7 02 80020000 / C7 01 E0010000 are shown unchanged but the target values in comments become 1066/600 — the immediates to actually write are 0000042A and 00000258. | src: 8B 15 A0C94100 mov edx,[glide3x.dll+0x1C9A0] (Glide window width addr); C7 02 80020000 mov [edx],00000280 (640->1066 0000042A); 8B 0D 2CC84100 mov ecx,[glide3x.dll+0x1C82C] (height


Author flags glide3x as hardest because Cheat Engine relative instruction addresses differ from hex-editor addresses: 0xC0D5-0xC0EC (hex editor) == 0xCAD5-0xCAEE (Cheat Engine). | src: 'relative instruction addresses that show up on Cheat Engine are not the same relative addresses that are shown in a hex editor'

hexed 0xC0D5..0xC0EC = CE 0xCAD5..0xCAEE


Fills the LEFT half of the screen. First draws a solid black rectangle over the left half to block transparent-DC6 cheats, then tiles the config screen-background DC6 across it. This is the perimeter/letterbox fill on the left of the 640-wide play area. | src: ::d2::d2gfx::DrawRectangle(0, 0, resolution.width / 2, resolution.height, 0, ::d2::DrawEffect::kNone);


Fills the RIGHT half of the screen. Draws a black rectangle from x=width/2 to x=width over full height, then tiles the background DC6 rightward. Left+Right together produce the full perimeter fill around the centered 640x480 UI region. | src: ::d2::d2gfx::DrawRectangle(resolution.width / 2, 0, resolution.width, resolution.height, 0, ::d2::DrawEffect::kNone);


Tiling loop: half_num_frames = GetNumFrames()/2; rows alternate row_start_frame = (row%2)*half_num_frames; columns drawn until width_covered reaches (width/2)-1. Left screen anchors DrawPositionXBehavior::kRight and draws from (width/2 - width_covered); right screen anchors kLeft and draws from (width/2 + width_covered). Both use position_y_behavior kTop. Row loop condition compares height_covered against resolution.width (note: width, not height — as written in source).


Original 800x600 border-frame DC6 path used when IsUseOriginalScreenBorderFrame() is true. | src: constexpr std::string_view kOriginalScreenBorderFrameImagePath = "data\\global\\UI\\Panel\\800BorderFrame";


Centering math for the original 800-wide border frame around the 640-wide play area. left = (width-640-(800-640))/2 = (width-800)/2; top = ((height-480-(600-480))/2)-3 = ((height-600)/2)-3. This is the (width-640)/2 family of centering offsets the task references, adjusted for the 800x600 frame art. | src: const int left = (resolution.width - 640 - (800 - 640)) / 2;  const int top = ((resolution.height - 480 - (600 - 480)) / 2) - 3;


Right-side original border frame anchor. right = (width+640+(800-640))/2 = (width+800)/2; top same as left ((height-600)/2)-3. | src: const int right = (resolution.width + 640 + (800 - 640)) / 2;  const int top = ((resolution.height - 480 - (600 - 480)) / 2) - 3;


Custom border frames center the 640x480 UI: screen_left = (width-640)/2, screen_top = (height-480)/2, screen_right = (width+640)/2. Vertical centering of the side borders uses (256+176)=432 as the reference inner-panel height: border_top = screen_top + ((432 - border_height)/2). | src: const int screen_left = (resolution.width - 640) / 2;  const int screen_top = (resolution.height - 480) / 2;


Public interception entry (extern "C" __cdecl). Reads d2client::GetScreenOpenMode() and dispatches: kRight -> DrawRight{Background,BorderFrame,BackgroundRibbon}; kLeft -> DrawLeft*; kBoth -> both; kNone -> nothing. Then calls original d2::d2client::DrawScreens(). Declared in replacement.hpp. | src: extern "C" void __cdecl Sgd2fr_D2Client_DrawScreenBackground();


1.13c call-branch patch that redirects into Sgd2fr_D2Client_DrawScreenBackground via the asm shim InterceptionFunc01. Found by tracing the 'ACT%d' string usage to the function return. Other versions: 1.09D 0x8696B, 1.10 0x81F08, 1.12A 0x90216, 1.13D 0x1D4A6, LoD1.14C 0x5264D, LoD1.14D 0x56FAF (all size 5).


NOP patch removing the game's original left-screen 800BorderFrame draw. Located via the 'Panel\800BorderFrame' string (two usages; this is the left one). 1.09D 0x58F1B(sz0x96), 1.10 0x5F4EB(0x9B), 1.12A 0x8218D(0x9A), 1.13D 0x6D2DD(0x9A), LoD1.14C 0x94B0C(0xAA), LoD1.14D 0x9865E(0x9E).


NOP patch removing the game's original right-screen 800BorderFrame draw. 1.09D 0x58FF0(0xAC), 1.10 0x5F5C0(0xAC), 1.12A 0x82094(0xB9), 1.13D 0x6D1E1(0xBD), LoD1.14C 0x94BF1(0xB3), LoD1.14D 0x98733(0xA7).


THE origin of the -320 render shift. Recomputes D2's screen-shift so the 640x480 play area is centered. screen_shift_x = (general_display_width/2) - (640/2); screen_shift_y = -((general_display_height/2) - (480/2)). Note the 640/2 = 320 term and the NEGATION on the Y term. For a 1280x720 display: shift_x = 640-320 = 320, shift_y = -(360-240) = -120. The '-320'-family value is the (640/2) offset that recenters the viewport origin. Uses d2client::GetGeneralDisplayWidth/Height a | src: int screen_shift_x = (general_display_width / 2) - (640 / 2);  int screen_shift_y = -((general_display_height / 2) - (480 / 2));  ::d2::d2client::SetScreenShiftX(screen_shift_x);


Call-branch patch feeding Sgd2fr_D2Client_SetScreenShift. Found via write-breakpoint on ScreenShiftX/Y variable. 1.09D 0x865BF, 1.10 0x81B8F, 1.12A 0x90166, 1.13D 0x1D3F6, LoD1.14C 0x5259B, LoD1.14D 0x56EFD (all size 0x27 = 0x...1D-0x...F6).


Struct the game passes to the tile-culling function. Layout verified by static_assert: flags at 0x0 (uint32_t), draw_window_rect (RECT) at 0x4, tile_culling_window (RECT) at 0x14 (20). RECT is 16 bytes (left/top/right/bottom int32). | src: struct CullingSpec { uint32_t flags; RECT draw_window_rect; RECT tile_culling_window; }; static_assert(offsetof(CullingSpec, flags)==0); static_assert(offsetof(CullingSpec, draw_wi


Sets the map/tile draw and culling windows. draw_window_rect = SetRect(left,top,right,bottom) from args. tile_culling_window base = SetRect(-80, -80, width+80, height+40) using GetGeneralDisplayWidth/Height. If perspective mode enabled, expands top/left/right/bottom by ratios (866px up = -204.375 unit space; ratio = 1732/(1732-height/2) - 1, capped at 1.0; bottom uses ratio = 1 - 1500/(1500+height/2)). Finally sets culling_spec->flags |= 0x1. The -80/-80/+80/+40 margins are t | src: SetRect(tile_culling_rect, -80, -80, width + 80, height + 40);  ... culling_spec->flags |= 0x1;


Jump-branch patch to the tile-culling replacement (via patch_1_11_shim). Found via 'Perspective' Video-Options string -> first D2Client-space call. 1.11 0x5B560, 1.11B 0x927A0, 1.12A 0xBA990, 1.13ABeta 0x3F490, 1.13D 0xB5330, LoD1.14A 0xB5330 (all size 5). Separate patch_1_07.cpp covers 1.07-1.10 & 1.14A-D with different offsets (e.g. 1.09D 0x16000, LoD1.14C 0x71D30, LoD1.14D 0x76000).


Sets the D2GDI cel horizontal draw window to the full width: SetCelDisplayLeft(0), SetCelDisplayRight(resolution.width). Resolution from GetIngameResolutionFromId(resolution_mode). This is the render-window widening that removes the stock 640-wide clip so the perimeter can be drawn. | src: d2::d2gdi::SetCelDisplayLeft(0);  d2::d2gdi::SetCelDisplayRight(resolution.width);


Jump-branch patch in D2GDI (not D2Client) for the cel display left/right. 1.11 0x6529, 1.11B 0x7239, 1.12A 0x6599, 1.13ABeta 0x65F9, 1.13C 0x7049, 1.13D 0x7E99 (size 0x2E each). Library = kD2GDI.


Feeds the resolution into the GeneralDisplayWidth/Height globals that everything above reads: SetGeneralDisplayWidth(resolution.width), SetGeneralDisplayHeight(resolution.height). Also cycles SetInventoryArrangeMode(inventory_arrange_mode++) as a workaround to stop inventory arrangement from transferring (overflow intentional). | src: d2::d2client::SetGeneralDisplayWidth(resolution.width);  d2::d2client::SetGeneralDisplayHeight(resolution.height);


Re-anchors a PositionalRectangle from the source (inventory-arrange) resolution center to the current resolution center, preserving width/height. dist = out.Left - source.width/2; out.Left = current.width/2 + dist; out.Right = out.Left + rectangle_width. Same for top/bottom about height/2. Uses GetIngameResolutionFromId(GetResolutionMode()) and GetSourceInventoryArrangeResolution(). | src: out_position_wrapper.SetLeft((current_resolution.width / 2) + dist_from_rect_left_to_display_center);


Like RealignPositionFromCenter for X, but anchors Y to the display BOTTOM: dist_from_rect_top_to_display_bottom = source.height - out.Top; out.Top = current.height - dist; out.Bottom = out.Top + rectangle_height. Used for bottom-docked UI (belt/skill bar).


NOPs the D2GFX.DrawRectangle call that draws the black rectangle on the Scroll-of-Inifuss (Cain quest) screen, which otherwise mis-renders at high res. Located via the '%s\UI\MENU\scroin' string then scrolling up to the DrawRectangle call. Single NOP patch per version. 1.13C at D2Client +0x7C38D (size 0x2D = 0x7C3BA-0x7C38D); 1.09D 0x8330C, 1.12A 0x70FBD, 1.13D 0x7516D, LoD1.14C 0x9C20D, LoD1.14D 0x9FA20 (all size 0x2D). Applies to essentially all versions 1.07-1.14D.


DrawLeftScreenBackgroundRibbon / DrawRightScreenBackgroundRibbon tile horizontal and vertical 'ribbon' DC6s across the perimeter beyond the border frame, using inner-panel reference height (256+176)=432 for vertical centering; border_left = screen_left - firstFrameWidth; loops fill until width_covered/height_covered exceed borders. Config-gated by IsScreenBorderFrameEnabled(); original-vs-custom chosen by IsUseOriginalScreenBorderFrame().


Sets viewport + scissor from a Rect, then updates the Constants CB. Verbatim: CD3D11_VIEWPORT viewport{ (float)rect.offset.x, (float)rect.offset.y, (float)rect.size.width, (float)rect.size.height }; _deviceContext->RSSetViewports(1,&viewport); CD3D11_RECT scissorRect{ rect.offset.x, rect.offset.y, rect.size.width, rect.size.height }; _deviceContext->RSSetScissorRects(1,&scissorRect); then writes _constants.screenSize[0/1]=size.width/height, invScreenSize=1/size, flags[0]=NoAn | src: CD3D11_VIEWPORT viewport{ (float)rect.offset.x, (float)rect.offset.y, (float)rect.size.width, (float)rect.size.height };


CD3D11_RECT ctor is (left,top,right,bottom) but D2DX passes { offset.x, offset.y, size.width, size.height } -> right=size.width, bottom=size.height rather than offset.x+width / offset.y+height. Harmless when offset==0 (offscreen render at {0,0,gameSize}) but with a centered _renderRect (non-zero offset in Present) the scissor's right/bottom fall short by offset.x/offset.y. Viewport still places content correctly; only the scissor is mismatched. Load-bearing if chasing edge-cl | src: CD3D11_RECT scissorRect{ rect.offset.x, rect.offset.y, rect.size.width, rect.size.height };


Called 3 places. SetSizes: UpdateViewport({0,0,_gameSize.width,_gameSize.height}) (offscreen game-size target, offset 0). Present: called with game-size dims AND with _renderRect (the centered/scaled final blit to screen). WriteToScreen: UpdateViewport({0,0,_gameSize.width,_gameSize.height}). Only the Present-with-_renderRect path carries a non-zero centering offset.

RenderContext.cpp SetSizes / Present / WriteToScreen


Stores _gameSize/_windowSize. displaySize = (FullscreenDefault ? _desktopSize : _windowSize). _renderRect = Metrics::GetRenderRect(_gameSize, displaySize, !NoWide flag). Computes desktopCenterX=_desktopSize.width/2, desktopCenterY=(FullscreenDefault?_desktopSize.height/2:_desktopClientMaxHeight/2), reads GetWindowPosition() preferred position (used iff x>=0 && y>=0). Branches Windowed vs FullscreenDefault for placement. Then ClipCursor(); and if(_resources){ ResizeBackbuffer( | src: _renderRect = Metrics::GetRenderRect(_gameSize, displaySize, !_d2dxContext->GetOptions().GetFlag(OptionsFlag::NoWide));

RenderContext.cpp SetSizes(Size gameSize, Size windowSize)


SetBlendState(batch.GetAlphaBlend()); picks atlas via GetTextureCache(batch); SetShaderState with Game vertex+pixel shaders, atlas SRV (batch.GetTextureAtlas()) and Palette Texture1D SRV; then _deviceContext->Draw(batch.GetVertexCount(), startVertexLocation + batch.GetStartVertex()). No positional transform here — geometry comes straight from the vertex buffer; the only offset is the vertex START index. | src: _deviceContext->Draw(batch.GetVertexCount(), startVertexLocation + batch.GetStartVertex());

RenderContext.cpp Draw(const Batch& batch, uint32_t startVertexLocation)


Full layout (32 bytes, static_assert): float screenSize[2]={0,0}; float invScreenSize[2]={0,0}; uint32_t flags[4]={0,0,0,0};. NO translate/offset/textureOffset field exists. Therefore the GPU-side constants apply NO per-batch or per-geometry positional translate; any floor-vs-wall positional difference must originate in the CPU vertex data, not the CB. | src: float screenSize[2]; float invScreenSize[2]; uint32_t flags[4]; // static_assert size==32


First tries MetricsTable exact-match on desktop size (narrow vs wide variant). Fallback: loop scaleFactor=1..8 { size.width=desktop.width/scaleFactor; size.height=desktop.height/scaleFactor; if (desktop.height/(scaleFactor+1) <= 600) break; }. Height cap: if size.height>720 { aspect=desktop.width/desktop.height; size.width=(int32_t)(aspect*720); size.height=720; }. Constants: 600 (break threshold), 720 (max game height), scale range 1..8.