Skip to content

Constantine: renders and plays its first level (resolve program, host visibility tests, seeds) - #109

Merged
phobos665 merged 7 commits into
mainfrom
title/constantine
Oct 30, 2026
Merged

phobos665 merged 7 commits into
mainfrom
title/constantine

Conversation

@phobos665

@phobos665 phobos665 commented Oct 30, 2026 •

Copy link
Copy Markdown
Owner

Toolkit fixes from bringing up Constantine (5343000F, XDK 5849). With these it boots, plays its Bink intro movies, reaches the main menu and plays its first level at a native 60 fps, with Constantine and his apartment drawn. The title project itself stays local (titles/ is ignored).

Changes

  • An immediate-mode draw at End runs the program the XDK started (c3bd8eba). Swap scales the 1280x480 supersampled screen down with one triangle: the XDK starts a two-instruction program (oPos = v0, oT0 = v9) through the push buffer and calls Begin / End. End drew it under the title's 2D program, which reads its texcoord from v2, so every pixel sampled one texel and each frame after the movies was a single flat colour. End now uses pushed_program_begin as the walk's own draws do.
  • Visibility tests are answered from host occlusion queries (dd28b57d). They were answered "0 pixels". Constantine culls its scenery and its hero by them (53 a frame), so neither was drawn.
    • The walk now hands SET_ZPASS_PIXEL_COUNT_ENABLE (0x17CC) and GET_REPORT (0x17D0) to the HLE.
    • The RHI counts samples in between: new rhi_occlusion_*, one Vulkan query per draw, one D3D11 query per test.
    • GetVisibilityTestResult answers with the matching test's count, scaled to guest samples.
    • All closed tests are read back in one GPU wait. One at a time it was about 80 waits a frame.
    • RECOMP_HLE_D3D8_VISIBILITY=0 switches it off.
  • Frame dumps and F11 see frames shown through the video overlay (39dbec03). They now catch movies presented by UpdateOverlay without a Swap.
  • A thread that only polls the clock sleeps between polls. Constantine's Bink player checks whether the next frame is due (a mutex taken and released) and reads the TSC in a loop between movie frames: about two million kernel calls a second, 83% of a core, during every movie. A thread that reads the TSC 256 times within 1 ms, four windows running, now sleeps 1 ms with the guest lock dropped. RECOMP_TSC_POLL_SLEEP=0 turns it off. Over 50 s of intro movies, main-thread CPU went from 44.2 s to 6.3 s, with the same movie frames at the same times. In-level uncapped is unchanged.
  • A switch's index walk stops at code something names (f61788a5, tools/disasm). The disassembler's walk over a switch's index bytes treated 56 8B (push esi; mov esi, ecx) as two more indices, because the switch has about 156 cases. It started a function in the middle of the mov, so the lifted method lost esi = ecx, its vtable calls went wild, and Select Difficulty showed no options. The walk now stops at a plausible function start that's named as code. Compared with the old disassembler, Constantine differs by exactly that one bogus entry. Other titles change only when re-lifted.
  • Seeds for Constantine (259d78e7): six thread starts, plus a callback (0x001DF280) that the disassembler had run into from a jump table before it. It was called over 1000 times unresolved.
  • 1e4536a8 is the same immediate-mode vertex fix as in Xbox Live without a console backup: generated console, Insignia registration, updater from the player's dashboard #108, cherry-picked here.

Measured (macOS arm64, Vulkan)

  • In-level, capped: a steady 300 swaps per 5 s (60 fps).
  • Uncapped: about 430 swaps per 5 s, against about 505 with RECOMP_HLE_D3D8_VISIBILITY=0. That run also draws less, because the culled objects aren't drawn.

Not done / needs checking

  • The D3D11 half of rhi_occlusion_* has not been built or run. It needs a Windows build, plus the TS2 and BLiNX regression before merging, because the End change, the visibility tests and the clock back-off affect every title.
  • Still open on Constantine:
    • 7 shadow-map draws read all-zero vertices;
    • 4 of 8 sound buffers are never heard.

…self

With Begin and End replaced and SetVertexData* not (the 5960 online
dashboard: its symbols are not identified), the vertices between them
are in the push buffer, written by the title's own code, and End's
push_skip walked past them without drawing: every such quad had no
vertices. End now walks them in while the group is open. A title whose
SetVertexData* are replaced writes nothing there, so nothing changes
for it.
D3DDevice_End drew its vertices under the shader the API last selected.
A program started through the push buffer since then is what the NV2A
runs, and the walk's own draws already use it (pushed_program_begin);
End now does the same.

Constantine resolves its 1280x480 supersampled screen inside Swap with
one triangle drawn this way: the XDK starts a two-instruction program
(oPos = v0, oT0 = v9) and calls Begin / End. Under the title's 2D
program, which reads its texture coordinate from v2, every pixel
sampled one texel and each frame after the intro movies was a single
flat colour. The main menu now draws.
A title that plays its movies through D3DDevice_UpdateOverlay presents
without Swap, so RECOMP_HLE_D3D8_DUMP and F11 never saw those frames.
The overlay's own present now dumps first. Found on Constantine, whose
Bink intro movies go this way.
GetVisibilityTestResult answered every test "0 pixels" once a title spun
on it: nothing executes the push buffer, so no report is ever written.
Constantine culls its scenery and its hero by these tests, 53 a frame in
its first level, and Constantine himself and most of his apartment were
never drawn.

The push-buffer walk now hands SET_ZPASS_PIXEL_COUNT_ENABLE (0x17CC) and
GET_REPORT (0x17D0) to the HLE. Between the two the RHI counts the
samples of each host draw (rhi_occlusion_*: one Vulkan query a draw, so a
test can span a render-target change; one D3D11 query a test), and
GetVisibilityTestResult answers with the count of the newest closed test
whose report offset names its index (bits 4-11 of the offset, the XDK's
16-byte slots in a 4 KB page). Counts are scaled to guest samples. All
closed tests are read back together, one GPU wait a batch: one at a time
it was ~80 waits a frame and cost a fifth of the frame rate.

Capped, Constantine's level holds 60 fps; uncapped ~430 swaps / 5 s
against ~505 with RECOMP_HLE_D3D8_VISIBILITY=0, which also draws less.
The D3D11 half is not yet built or run on Windows.
From a run's log (tools.seed_from_log). 0x001DF280 is a callback the
title stores at 0x35A3F4; the disassembler ran a six-entry jump table at
0x1DF268 into it, so it was lifted as the middle of sub_001DF1B0 and
called over 1000 times unresolved.
Constantine's Bink player asks "is the next frame due?" -- a mutex taken
and released -- and reads the TSC round and round between movie frames:
about two million kernel calls a second, 83% of a core, during every
movie. The console spins the same way, but on a handheld it is heat and
battery for nothing.

A thread that reads the TSC 256 times within a millisecond of host time,
four windows running, now sleeps 1 ms (xbox_GuestSleep, so the guest lock
is dropped) and reads the clock afresh. A loop waiting for a moment
overshoots it by about that much at most; work that reads the clock as it
goes reads it far less often. RECOMP_TSC_POLL_SLEEP=0 lets it spin.

Constantine's intro movies, 50 s: main-thread CPU 44.2 s -> 6.3 s, mutex
waits 77.7M -> 5.4M; the same movie frames at the same times and the
movie's sound consumed at the same rate. In-level, uncapped, unchanged
(~438 swaps / 5 s).
The pass that finds the function after a switch's tables walks over the
byte index table, taking every byte below the dword table's entry count
as an index. A switch with more cases than the next function's first
bytes are worth cannot tell the two apart by value: Constantine's at
0x0004E220 has ~156 entries, so "56 8B F1" (push esi; mov esi, ecx) at
0x0004E490 read as two more indices, a function was started at the F1 in
the middle of the mov, and the lifted method lost "mov esi, ecx". Its
vtable calls went through whatever esi held -- the ICALLs to 0x804AF540
and 0 -- and the Select Difficulty menu was drawn with no options.

The walk now stops at an address that is named as code -- a function or
candidate start, a direct branch from inside a function, or a data word
beside another code pointer (a vtable; XBEs flag .rdata executable, so
code sections are told by name) -- and could start a function (first
byte 0x0F or above, a body that decodes). The usual skip at the walk's
end still looks only at existing starts, so a named function after the
tables is still recovered here. Branches decoded from the index bytes
themselves, lone data words and alias entries are not evidence: each
lost a real function in a first version of this.

Constantine against the previous disassembler: one entry fewer
(0x0004E492, the split) and nothing else changed; its difficulty menu
draws Easy, Medium and Hard.
@phobos665
phobos665 merged commit a00271a into main Oct 30, 2026
7 checks passed
@phobos665
phobos665 deleted the title/constantine branch October 30, 2026 15:56
bechapm added a commit to bechapm/DirectXbox-Recomp-D3D9 that referenced this pull request Nov 1, 2026
Catch up with phobos665 main (phobos665#107, phobos665#109), and D3D9 occlusion queries
bechapm pushed a commit to bechapm/DirectXbox-Recomp-D3D9 that referenced this pull request Nov 1, 2026
Brings this fork's main level with upstream: phobos665#109 (Constantine: resolve
program, host-counted visibility tests) and the touch-control default.
No conflicts. feat/d3d9-remix already has both (PR #7).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KicvamdgSFkngztRkMUVsV
bechapm added a commit to bechapm/DirectXbox-Recomp-D3D9 that referenced this pull request Nov 2, 2026
Bring main level with phobos665 main (phobos665#109, touch default)
@phobos665
phobos665 restored the title/constantine branch November 2, 2026 09:02
@phobos665
phobos665 deleted the title/constantine branch November 2, 2026 09:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant