Deus Ex: Invisible War demo

Original media: test/binaries/win98-games-a-d/DeusEx Invisible War-D3D. All 16 local files (236357810 bytes) were hashed and transferred unchanged to bx_hx8msa33:/home/user/deusex-iw-media-20261010. Source identity inventory: scratch/deusex-iw-20261010/selection.json and the self-contained first run scratch/runs/20261010T1527Z-deusex-iw-startup/selection.json.

InstallShield version 7 cabinets are readable by tools/is-cab.js. Extraction verified MD5 and lengths for 816 entries. Descriptor 31, setup.skin, failed verification: do not report a complete installer. Conflicting setup resource filenames were preserved separately under .conflicting-entries/<index>/; identical duplicates share a canonical file. The receipt records every entry, conflict and failure. No installer or licence dialog was answered.

The game executable DX2.exe is 6090752 bytes, SHA256 da394a4b5eb6254d9bee648b4bb1bee557b516cca61bb24b55020d29bab72f46. Its direct imports are DInput8, D3D8, DSound and Windows APIs; no original non-system DLL appears in its direct import list. Extracted files remain on the boat at /home/user/deusex-iw-extracted-20261010. There is no local corpus registration or public deployment.

Reconstructed virtual layout

The archive directory tree is not the installed tree. The original Default.ini refers to ..\Content\DX2; EXE strings refer to ..\System\Shaders. Use C:\System\DX2.exe, working directory C:\System, and these tested mounts:

This is an investigation mapping, not proof of every install destination. The startup probe mounts 808 canonical files eagerly; a required/lazy policy is not yet qualified. Keep original config and executable bytes unchanged.

Startup evidence, 2026-10-10

Browser: headless Chrome 151, default WebGL/guest Worker, 1000x800, no inputs. Source cedee9b54; module SHA256 c43dd314eb864e55c395c0617cfc89e51a018613e8ae3aba7f2c45fde58ea5e6.

Data/normalize.dds SHA256 is d2a3eec5d57593fe5ee5bd840d71cca958826f4813908689e7ea5e0bb708f27d. Its DDS header reports 256x256, 32-bit RGB, caps 0x1008, caps2 0xFE00 (cubemap/all faces), and no mip count. The failed texture lookup is resolved; do not repeat downloads or infer a missing texture from the generic message. No menu, player-controlled gameplay, audio or FPS is qualified.

Cubemap format negotiation identified

20261010T1539Z-deusex-iw-d3d-trace uses the same source, module and original files, with the existing D3D API and filesystem traces enabled. It closes normally at 15:40:49Z with no browser errors. After reading Normalize.dds, the guest calls GetDirect3D, GetDeviceCaps and GetDisplayMode, then makes 43 CheckDeviceFormat calls: adapter 0, HAL, adapter format 22, usage 0, resource type 5 (CUBETEXTURE), with candidate formats including 21 and 22. Every call has guest return address 0x007fd881. The trace's ret= field is the caller address, not an HRESULT.

The exact tested handle_IDirect3D8_CheckDeviceFormat only accepts ordinary textures (type 3) and supported target/depth surfaces (type 1). Therefore all these type-5 queries return D3DERR_NOTAVAILABLE (0x8876086a). This is established by the handler and captured arguments, not a dynamic HRESULT capture. The guest releases IDirect3D8 at return address 0x007fd8ce and shows the initialization error. It never calls CreateCubeTexture.

The current rejection is honest: Device8_CreateCubeTexture is fail-fast, and the D3D8 caps deliberately omit cubemaps. The next implementation must provide the real D3D8 cube interface and storage before advertising support. D3D9 already has cube allocation, face/mip storage and sampling, but its vtable cannot be reused directly: D3D8's base texture omits three D3D9 methods, and its surface descriptor layout differs. Cover the D3D8 ABI, face/level locks and surfaces, lifetime, format/create agreement, and a rendered cube sample; then repeat this original startup route. Do not patch the guest, change the DDS, or return success just to bypass negotiation.

D3D8 cube implementation validation

The candidate adds a separate 19-slot CubeTexture8 interface over the shared cube storage. It adapts surface descriptors, preserves Cube8 identity through QueryInterface/GetContainer, and exposes cube formats and mip/filter caps. Unsupported private-data methods retain the existing fail-fast behavior. ABI references: Wine's D3D8 interface declarations and D3D8 capability definitions.

test/test-d3d8-cube-texture.js fails on the baseline's cube-format rejection. The candidate passes calls through actual generated COM slots, exact stdcall cleanup, all six faces and three levels for ARGB/XRGB/DXT1/DXT5, descriptor sizes, shared surface locks, interface identity, and retained parent lifetime. Existing D3D9 cube and D3D8 16-bit regressions and the full build pass too. The shared sampler's separate WebGL1/2 test passes 48 face/stage/vertex-linkage cases; this is renderer coverage, not evidence of Invisible War gameplay. Validation logs are retained with the candidate's browser run.

20261010T1552Z-deusex-iw-cube proves the original game advances past the format rejection: it creates a 256x256 XRGB cube, obtains each of its six surfaces, calls GetDesc/LockRect/UnlockRect/Release for every face, loads the next 2D texture, and sets both texture priorities. The same generic dialog then appears after GetDeviceCaps returns to 0x004b7496. Browser closure is normal at 15:54:03Z; the final screenshot still shows the startup error.

Disassembly retained in next-caps-disassembly.txt shows the next checks: TextureCaps bits 0x800 and 0x400, then PixelShaderVersion at caps+0xcc must be at least 0xffff0101 (ps_1_1). The candidate supplies the texture bits but truthfully reports zero for D3D8 programmable pixel shaders. Implement the real D3D8 shader handle/create/bind/delete/constants path before changing that capability; do not fake the version to skip this check. This is progress through initialization, not a playable-game claim.

Pixel-shader handle adapter

The candidate uses a device-owned handle table over the shared shader objects, with an internal reference for each live handle. Creating a handle does not leave a COM reference retaining the device. GetPixelShader returns the DWORD without AddRef; DeletePixelShader invalidates the handle and unbinds it when current. Captured state blocks retain the bytecode independently. Tombstone identities survive until device destruction so restoring captured state can report the old handle without making that handle valid again for Set/Delete. The final device release retires undeleted handles as well as ordinary binds. These semantics follow Wine's D3D8 device implementation.

The implementation delegates bytecode validation, retained IR, constants and execution to the shared backend, including main's eaf7d0316 create/draw validation repair. D3D8 now reports ps_1_4 and MaxPixelShaderValue 8, backed by the real create/bind/delete/function/constant paths, rather than just changing the capability check. Vertex shader support is still not advertised.

The baseline traps at CreatePixelShader in the new handle regression. The candidate passes invalid/deleted/foreign handles, immutable bytecode, constant round-trips, Get without retaining, captured-state restoration and final device cleanup. D3D8-created/bound retained IR renders 16 expected-color frames across ps_1_1..1_4 and WebGL1/2. Shared shader lifetime and cube tests pass, as do 15 PS1.4 software/WebGL differential frames and the full build. The quiet-handler ratchet removes only the old SetPixelShader constant-return handler (242 -> 241); no new quiet handler is introduced.

20261010T1611Z-deusex-iw-pixel reaches the Eidos intro and then the game's branded loading screen using original files and no inputs. The reviewed final capture is a loading screen, not a menu or gameplay. Chrome exits normally at 16:13:15Z, with no page errors. The trace records 54 CreatePixelShader calls before its 12000-line cap fills during Bink video reads; it cannot establish what happens later. Follow up with a longer ordinary-input route and narrower tracing. The actual browser source is eaf7d0316 plus the retained candidate patch, not the later merged source. Audio and FPS remain unqualified.

Loading exit on merged source

Three further routes use exactly dc2e0d410, the unchanged original files and the reconstructed mounts above:

The original block reads [edi] and calls its virtual method at +0xa8 (0x0069a3e4). The caller obtains this object through 0x00699660, whose factory virtual call returns at 0x00699680; its result is copied into EDI. The null producer still needs dynamic tracing. Original disassembly and controller are retained in the boundary run directory.

The trace also records CreateVertexShader calls with a non-null function pointer. The current D3D8 adapter supports declaration-only calls and rejects these with D3DERR_INVALIDCALL. That is a real unsupported path, but its causal connection to this null object has not been established. Do not infer a vertex-shader fix from temporal proximity alone or advertise unsupported caps. There is still no verified menu, ordinary gameplay, audio or FPS.

Missing UI configuration and per-thread image identity

Runs 20261010T1630Z-deusex-iw-factory and 20261010T1640Z-deusex-iw-ini-focused narrow the null producer to the GeneralHUD class lookup: 0x698f90 calls configuration helper 0x416b40, which returns AL=0 at 0x698fe5 after two failed opens of C:\DX2UI.INI. The resulting class ID is -1. The original DX2UI.ini contains Type=GeneralHUDWindow; no shader change is justified by this failure.

Original cabinet file-group descriptors establish the installation layout: NewComponent1 (46..1355) targets <TARGETDIR>\Content, NewComponent2 (1356..1513) targets <TARGETDIR>\System, and UserIni (1517) targets <TARGETDIR>. DX2.exe (1364), DX2UI.ini (1376), and Data/normalize.dds (1407) are all in the System group. The earlier root Data mount was a diagnostic workaround, not the installer layout.

The original path helper reads REG_SZ ION_ROOT_PC_DEMO in HKLM\SOFTWARE\Ion Storm\Deus Ex - Invisible War Demo. Seeding it with C:\ selects the expected Documents directory but does not cure the root fallback. The System-directory value is incorrect: the isolated comparison 20261010T1704Z-deusex-iw-registry-only looks for the logo under C:\System\content\dx2\VideoTextures, whereas the installer puts Content under TARGETDIR. That run ends at a blank desktop, not gameplay.

20261010T1709Z-deusex-iw-path-helper captures the main thread returning the correct C:\System\DX2.exe from GetModuleFileName at 0x4130fe, and the correct System directory from helper 0x4130b0 at 0x413c46. A separate two-instance control on the same build proves that a new thread reports only C:\System\ (length 10). Its instance-local image-name length remains the default seven. A regression that instantiates the child *after* setting the name additionally exposes the active-data initializer overwriting the old 0x120 name buffer with app.exe.

The candidate fix publishes name bytes, pointer, length and drive in a process-owned region without an active data initializer. init_thread inherits the metadata; the launch setters keep their existing interface. The focused dynamic-module-filename test now checks both the main and a newly created instance with a nested executable path on drive D. It fails on the old implementation and passes with the candidate. Full build gates and the inherited-global regression pass. In the original game comparison 20261010T1717Z-deusex-iw-process-image, guest thread 1 now opens and reads all 56593 bytes of C:\System\DX2UI.INI. The reviewed 60-second and final 180-second frames remain on the loading screen. Chrome closed normally at 17:20:10Z, with no page errors. The config failure is fixed; the remaining loading stall needs its own thread/render boundary diagnosis. There is still no menu, ordinary gameplay, FPS or audio qualification.

Longer loading control after the image-path fix

20261010T1723Z-deusex-iw-loading-threads on exact 6bc406dc5 shows main-thread EIP changing across snapshots. A render wait at 90 seconds has cleared by 120 seconds; secondary thread 1 waits at 0x41fb2f. Do not call that a fixed renderer deadlock.

The no-input 20261010T1726Z-deusex-iw-loading-long runs 600 seconds. Its reviewed final frame still shows loading. Render commands submitted/consumed/ completed all equal 21804, with zero inflight bytes and no queue error. The main API count advances to 8940564. The trace is still reading the original Content\DX2\VideoTextures\intro.bik late in the run. Chrome closes cleanly at 17:36:46Z. Neither a menu nor gameplay has been established.

20261010T1738Z-deusex-iw-intro-input sends ordinary focus clicks and Escape at 35/70/110 seconds, then a two-second Escape hold at 17:42:03Z on the same browser. The final reviewed 240-second frame still shows loading; cleanup is normal at 17:42:59Z. The keyboard queue records hwnd=0, which is the intentional Worker routing convention, not proof of a focus bug.

The original Intro.bik is only 204 bytes. Local ffprobe on the boat reports Bink video, 320x240, 30 fps, duration 0.033333 seconds. Thus repeated reads are not evidence of a long cinematic progressing normally. Next trace the guest video completion/menu transition; do not patch files or assume the known unsupported vertex-shader path causes this loop.

D3D8 vertex programs and original shader corpus (2026-10-10)

The passive Bink completion probes observed four completions, including both startup-logo consumers. Repeated intro reads alone do not establish an endless Bink wait. The subsequent D3D8 control exposed a separate missing adapter: identical VS1.1 bytecode succeeded through D3D9 and failed through D3D8.

The adapter now retains a device-owned program/declaration pair, original declaration tokens and source bytecode, and explicit input-register mapping. The real game declaration at VA 0x9b49c4 uses registers v0..v4 with FLOAT3, D3DCOLOR, FLOAT2, D3DCOLOR, D3DCOLOR. Sparse v0/v7 controls verify that shader inputs are not inferred from fixed-function semantics. Tests cover queries, constants, stale/foreign handles, state blocks and device cleanup.

The extracted Shaders directory contains 44 vertex programs: 41 have version 0xfffe0100 and three have 0xfffe0101. Merely enabling the VS1.1 adapter accepts only the latter three. The legacy version must remain intact through native validation, retained bytecode, IR projection and software/GL compilation. Wine's d3d8 test_validate_vs explicitly accepts a VS1.0 version word.

Twelve original VS1.0 programs also encode m3x3 with a default xyzw destination mask (first instance at DWORD27, opcode0x17). Legacy normalization emits only the instruction's row count, preserving the remaining destination components, matching Wine shader_glsl_mnxn. The explicit-mask requirement remains for VS1.1, and arbitrary partial masks still fail. This is implementation-reference evidence, not a native Windows conformance measurement.

Evidence: runs/20261010T1830Z-deusex-iw-vertex-corpus records the initial refusals; runs/20261010T1834Z-d3d8-legacy-vertex accepts32/44 unchanged programs; runs/20261010T1839Z-d3d8-legacy-matrix accepts44/44 unchanged programs. The five-matrix regression fails before normalization and passes afterward, including untouched components; the real host/software/WebGL1/WebGL2 pixel checks pass. Full candidate build passes. The VS1.0-only 180-second original run still shows loading; the full-matrix 180-second comparison also remains at loading (reviewed runs/20261010T1839Z-deusex-iw-matrix, cleanup18:42:48Z). It now paints the v1.1 label above the loading graphic, but no menu/input/playability is proved. Shader acceptance is not gameplay qualification.

References: https://github.com/wine-mirror/wine/blob/master/dlls/d3d8/tests/device.c and https://github.com/wine-mirror/wine/blob/master/dlls/wined3d/glsl_shader.c .

Single-level cube sampler refusal (2026-10-10 18:54Z)

On merged4b2494187, passive uploadTexture instrumentation captures the next refusal: stage1 is a64x64 cube with one mip level, min=mag=LINEAR, mip=LINEAR, LOD bias=-1 and MAXMIPLEVEL=0 (runs/20261010T1847Z-deusex-iw-sampler-refusal). The host snapshot uses the same level count for every cube face.

Every LOD clamps to the sole level. With equal min/mag filters, both texture selection and filtering are independent of bias/clamp. Permit this neutral case while retaining explicit rejection for genuinely unsupported multi-level and unequal-filter cases. Validate finite float bias and uint32 mip clamp before taking the neutral path. No guest sampler values are patched.

The regression fails on main, passes288 WebGL1/2 face/stage/filter/bias/clamp cases on the candidate, and keeps invalid/multi-level/mixed-filter cases refused (runs/20261010T1850Z-d3d9-single-level-cube). Original180s comparison now reaches the3D menu scene (runs/20261010T1851Z-deusex-iw-cube-lod, reviewed final.png); the loading graphic is gone. Text and textures are visibly wrong (magenta panels, missing labels), and no player-controlled gameplay is proved. The1842-entry console has no D3D draw refusals; missing comctl32.dll is still reported. errors.json covers page/harness exceptions only, so inspect the console separately before claiming no renderer failures. Next investigate menu text/texture correctness and ordinary menu input on the merged fix.

Shader handles lost across Reset (10 October, 19:25 UTC)

The menu now accepts ordinary Enter and reaches character setup, but its geometry is white and the portrait/button label are absent. A bounded probe using the existing guest EIP breakpoint facility captured actual API returns at 0x004b56b3 (SetVertexShader) and 0x004b56df (SetPixelShader). Nonzero handles returned D3DERR_INVALIDCALL (0x8876086c); both handle-list heads in the live device program state were zero. Fixed-function selections succeeded. The earlier API trace's ret= field is a caller address, not an HRESULT. Evidence: scratch/runs/20261010T1922Z-deusex-iw-binding-return.

The shared Reset transaction allocated replacement program state but did not transfer D3D8's vertex/pixel handle registries or monotonic pixel handle counter. Preserving those device-owned fields retains programs and deleted-handle tombstones while the normal Reset path still clears current bindings. Wine's D3D8 test_reset creates a vertex shader, resets, then successfully deletes the same handle. This is reference-test evidence, not a new Windows reference run.

The regression exercises failed and repeated successful Reset, rebind/query, dead handles, counter uniqueness and device destruction with direct and worker software rendering. The unchanged-source control fails on the first post-reset vertex rebind. Original-game browser run scratch/runs/20261010T1930Z-deusex-iw-reset-handles now shows the blue setup panels, portrait and readable done button after ordinary Enter. The before/after pictures were reviewed. This verifies the setup rendering fix; player-controlled gameplay, audio and FPS remain unqualified.