Hype: The Time Quest demo
App: hype_glide_demo (experimental). Original executable:
test/binaries/candidates/hype-time-quest-demo/launch-game/MaiDFXvr_bleu.exe.
The executable directly imports 50 Glide 3 entry points. Static extraction,
English installer mappings, package provenance and generated Windows INI are
documented in the Glide 3 corpus report.
Startup DebugBreak: missing main-image export lookup
The 2026-09-29 remote CLI smoke stopped in the original sound plugin before
establishing Glide gameplay. The relevant log is
~/nfs-movsd/build/hype-glide-smoke.log on the reserved test box. It loaded:
| Image | Original base | Observed loaded base |
|---|---|---|
MaiDFXvr_bleu.exe |
0x00400000 |
0x00400000 |
dll/WAVx2BVR.dll |
0x10000000 |
0x04301000 |
Thread 1 stopped at runtime 0x04302129, preferred DLL VA 0x10001129.
The preceding instruction reads the callback cell at preferred
0x10024b9c; if it is zero, the plugin calls DebugBreak. After the break,
the wrapper would call that same pointer, so ignoring the break would enter
address zero rather than repair startup.
The plugin initializer at 0x10003fb0 first resolves
_SND_fn_vDisplayError@8 from the main executable handle. Its lookup helper
at 0x10003f40 calls GetProcAddress. If that returns zero, it constructs
the message Function cannot be loaded dynamically and invokes the error
callback that it has not yet initialized. Thus the visible break is the
error-reporting path for the failed export lookup.
The original executable does export the requested callback:
| Name | Ordinal | Original function VA |
|---|---|---|
_SND_fn_vDisplayError@8 |
2229 | 0x004d5430 |
_SND_fn_vDisplayErrorEx@12 |
2230 | 0x004d5490 |
The executable has 2,739 export-address entries, no zero holes and no
forwarders. Its export directory is RVA 0x194d70, size 115,602 bytes.
This was checked against the original file, not inferred from strings.
handle_GetProcAddress previously searched DLL_TABLE, then known native
API names. The executable is not a member of DLL_TABLE, so it could not
answer this lookup. The fix resolves exports from the main image before
that existing path, using the shared mapped DOS/PE headers to obtain the
export directory. It must not rely on the loader instance's
exe_export_rva global: Hype makes the call on a secondary guest thread.
No sound configuration, executable bytes, or DebugBreak behavior changed.
The existing test/test-getprocaddress-sparse-name.js now checks the callback
name and ordinal, out-of-range ordinals, a zero address-table slot, an absent
name, stdcall stack cleanup and operation with the loader-global RVA zero.
Its original sparse-heap Win32 lookup still passes. The focused test passed
remotely on 2026-09-29 after correcting the synthetic fixture's name pointers
to lie above the 16-bit ordinal range. The post-fix remote CLI run reached a
visible main menu without the startup DebugBreak; evidence is
build/hype-fixed-startup.log and build/hype-fixed-startup.png on the test
box. The screenshot was opened for review. Browser Glide identity, entry into
the playable world and input-driven movement remain pending; menu acceptance
alone does not establish gameplay.
Reproduction
Prepare the original extracted corpus, then use a current compiled artifact on the remote test machine:
node tools/prepare-glide3-corpus.js --check
node test/test-getprocaddress-sparse-name.js
node test/run.js --app=hype_glide_demo --threads --no-build \
--wasm=build/wine-assembly.wasm --quiet-api --max-batches=2000 \
--png=build/hype-fixed-startup.png
Static evidence can be reproduced without executing the game:
node tools/pe-exports.js test/binaries/candidates/hype-time-quest-demo/launch-game/MaiDFXvr_bleu.exe
node tools/disasm.js test/binaries/candidates/hype-time-quest-demo/launch-game/dll/WAVx2BVR.dll 0x10001120 0x10001142
node tools/disasm.js test/binaries/candidates/hype-time-quest-demo/launch-game/dll/WAVx2BVR.dll 0x10003f40 0x10003fe7
Export lookup limits
Native KERNEL32 currently aliases the main image handle. A missing EXE export therefore retains the existing native-API fallback; removing it would break callers looking up Win32 functions through that alias. An unknown callback still returns zero. This is compatibility with the current handle model, not evidence that distinct Windows module handles are interchangeable.
The reused resolve_image_export helper validates ordinal bounds and empty
address entries. It assumes valid name-to-ordinal indices and does not follow
PE export forwarders. The new dynamic main-image path guards the export
directory's RVA/size range: a resolved address inside it logs marker
0x46574452 (FWDR) and the address, then traps. It therefore cannot return
a forwarder string as callable code. Named and ordinal forwarder cases have
focused regressions, which also passed remotely. Hype has no
such entries. This is an explicit unsupported case, not general forwarded-export
support; malformed PE export tables remain outside this investigation.
Normal-input gameplay probe
The original launch-game/Readme.txt, section IX, documents the default
QWERTY bindings: arrows control the character, left Shift runs, Ctrl jumps,
Space performs an action, and Enter uses magic. Up/Down navigate menus;
Left/Right turn in the world. Do not assume a held Enter key moves the player.
Gamedata/Options/Default.cfg is binary data, not a text configuration to edit.
The existing investigation probe build/glide-app-probe.js can launch
hype_glide_demo webgl 120 build/hype-gameplay-webgl. It records desktop and
actual Glide drawable screenshots every five seconds, plus API version,
endpoint/backend and draw/present counters in states.jsonl. Its screenshots
are observations, not automatic world acceptance. A normal New Game menu
selection must be verified visually before calling a changed image gameplay.
After reaching the world, compare a stationary frame against a held ArrowUp
interval and a turn, while confirming API version 3, a glide endpoint and
advancing geometry/presents. The probe accepts investigator input through
input.json, for example [{"hold":"ArrowUp","ms":3000}]; this drives
ordinary browser input and does not modify the guest state directly.
Menu input, viewport and FIFO polling (2026-09-30)
The menu initially ignored held keys despite correct host physical-key state.
Original code at 0x48e3bd checks DIDEVCAPS.dwFlags & DIDC_ATTACHED before
enabling its keyboard. Our capabilities omitted that flag. Correct attached
keyboard/mouse reporting, plus the correct dwButtons offset 16, passes the
remote DirectInput regression for both 24-byte and 44-byte structures. Normal
Enter now loads the level. Keyboard flags change from 0x12 to 0x13, and
the guest poll counter advances. No focus or input-transport workaround was
needed.
The resulting level capture still occupies only part of the 640×480 drawable.
A read-only snapshot confirms the top window has a 640×480 client, while its
three nested children retain 388×268 clients. Hype initializes its logical
resolution globals 0x5d8108/0x5d810c to 640/480 and correctly selects Glide
resolution enum 7. Its viewport creation (0x49a730) uses the parent's client
rectangle, and DEV_Device::OnSize (0x499250) resizes the child. The Glide
fullscreen path changed geometry without the normal resize notification.
A candidate delivered WM_WINDOWPOSCHANGED after releasing the Glide lock,
allowing normal DefWindowProc processing to produce WM_SIZE. The subsequent
build/hype-full-world-webgl.log snapshot still had 388×268 child clients.
The candidate and its callback fixture were removed: it did not improve the
layout and its internal synchronous dispatcher bypassed window-owner thread
routing. A subsequent owner-thread fix is described below.
The matched hype-capture3-webgl / hype-no-notify-webgl captures also exclude
notification as a necessary cause of the observed bad geometry. Both capture
three triangles with nine vertices: five X values are NaN and all finite X
values are zero. Their draw state and non-finite field patterns match; all
eight drawable PNGs in both runs share SHA-256
d38b77b951118e53418317ab5ab5fea654c449b3dc757ede3c171383b4902ceb.
Static inspection narrows the next trace: CPA_MainFrame::OnSize at
0x499fe0 calls helper 0x477a70, which always returns zero. It therefore
takes the branch that conditionally waits on the application's semaphore
before calling native MFC42 ordinal 5030 through thunk 0x4f442c. That MFC
handler (preferred address 0x5f40df89) invokes its default handler and then
virtual method +0xd0 (frame layout) unless the size type is minimized.
Counting entries to 0x499fe0 and 0x499250, alongside child creation and
Glide open, will distinguish missing dispatch from notification timing or
layout suppression. No compositor scaling workaround is justified by this
evidence.
For upstream geometry diagnosis, original polygon clipper 0x483670 takes
the vertex count in ECX, a 60-byte-stride vertex buffer in EDX, and the renderer
context at [ESP+4]. Float clip bounds in that context are top +0x3daa8,
bottom +0x3daac, left +0x3dab0, and right +0x3dab4. Screen-quad producers
0x486a30 and 0x486d20 use staging buffer 0x835ae0 (four vertices), but
which produced the captured frame remains unverified.
The Y-edge interpolator at 0x483bf0 is a specific candidate for the NaNs:
it calculates (boundary-yA)/(yB-yA), interpolates X and attributes, and writes
the boundary directly as Y. This can produce the captured finite-Y / NaN-X
and attribute pattern. At its entry ECX is the output vertex, EDX is vertex A,
and stack offsets +4,+8,+12,+16,+20 hold vertex B, yA, yB, boundary, and
context respectively. Capturing these inputs and the pre-clip staging buffer
will distinguish invalid source geometry or bounds from arithmetic failure;
the instruction sequence alone does not establish which occurred.
The subsequent world stall is a separate query bug. The rendering thread
repeatedly reaches 0x4f1626, the grGet import thunk. Calls at 0x482427,
0x482442 and related sites ask for GR_FIFO_FULLNESS (3), length 8, and loop
while the first output word exceeds 2,000,000. The unimplemented query returned
zero without writing the output, leaving a stale stack value to drive an
infinite polling loop. The query now drains pending commands and waits for
backend completion (WebGL finish, synchronous native software), then writes
the SDK's free-entry count and status words. This adds no pixel readback.
ABI ordering, invalid-length and output-boundary tests pass remotely, as do
software and WebGL 1/2 completion tests. The render loop now progresses.
A subsequent captured frame still cannot change the picture: it clears only depth, then submits three invalid/degenerate triangles. Five of nine vertices have NaN X; the other four have X=0. Both software and WebGL retain the menu. This first post-load frame was not enough to characterize steady gameplay. The later matched notification comparison and frame captures below narrow the observation without establishing a CPU arithmetic bug.
Evidence on the remote machine: build/hype-attached-webgl/ contains the
first world capture; build/hype-world-diagnostics.log and its corresponding
states.jsonl record the window tree and stalled thread. These captures do
not yet demonstrate movement.
Later frame comparison
Matched runs build/hype-steady-default/ and
build/hype-steady-interpreter/ use the same no-notification WASM and normal
Enter, Space, ArrowUp and ArrowRight route. The second disables both the
micro-op tier and x87 folding. A frame captured after 800 presents contains
no geometry in the default run, versus 390 triangles with 1,170 finite
vertices in the interpreter run. This comparison does not isolate either
optimization or prove a CPU bug: the captured frames can represent different
points in the application's execution.
Interpreter screenshot 006-drawable.png shows the character in a blue
corridor, still restricted to 388×268 pixels. It was opened in Preview.
The other 15 drawable captures retain the exact earlier menu hash. LFB
read/write counts stop at 238 and remain unchanged through the later capture
interval, so repeated LFB uploads do not explain that return to the menu
image. Later actual-presentation captures below identify the alternating
guest swaps; this was not dropped delivery by the shared render worker.
Owner-thread resize and movement
Glide fullscreen open now posts WM_MOVE and WM_SIZE through the existing
owner-routed USER queue, matching DirectDraw's mode-change path. It does not
call the window procedure on the rendering thread or while holding the Glide
lock. The focused ABI regression opens from one instance with a window owned
by another, verifies the ordered payloads and absence of inline callbacks,
and checks that a failed open posts nothing.
On the trusted fallback server, build/hype-post-size-webgl/ captures a
636×476 child viewport inside the 640×480 drawable, replacing the earlier
388×268 viewport. Actual presentation frames in build/hype-swap-callers/
show normal ArrowUp movement and ArrowRight turning; world-present-661.png
and world-present-1047.png were compared visually and opened in Preview.
Default CPU settings in build/hype-default-callers/ also produce four
captured world frames with 403–406 triangles and no nonfinite fields.
The earlier single-frame optimized/interpreter comparison sampled opposite
halves of the UI/world alternation and does not establish a CPU bug.
The remaining empty UI swap still prevents stable visible presentation.
Alternating world and UI swaps
The eight adjacent swaps in build/hype-swap-callers/frame-capture.json
all originate from thread 1, return to 0x4671d4, and use interval 1.
World frames have higher caller 0x43f631; empty frames have caller
0x42252b. Device 0x03f012d8 has field +0x38 == 0, and both globals
0x77728c and 0x5da054 are zero throughout this sample.
Static disassembly explains the pair: frame-finish callback 0x43f610
(installed at 0x5b2290, paired with render callback 0x43f180 at
0x5b228c) finalizes descriptor 0x71cd04, swaps surface ID
short[0x71cd02], then directly calls 0x422260 at 0x43f678 before
releasing semaphore [0x71cd6c]. That second function acquires surface
short[0x71cd70] into descriptor 0x71cd74, visits UI objects from
[0x7136e0] through links at +0xd8 using 0x41f300, finalizes the
descriptor, and unconditionally calls the same swap wrapper. These are
nested guest paths, not two independently scheduled windows or threads.
Wrapper 0x467180 decodes its second argument as device index /16 and
surface index %16, clears the surface's acquired flag at +0x64, and
calls Glide swap when [0x77728c] == 0. Acquisition 0x467070 sets that
global from whether device field +0x38 is nonzero. No renderer suppression
is justified by this evidence. The follow-up capture records world surface
ID 0 and UI surface ID 1 on device 0, an empty UI-list head at 0x7136e0,
mode byte 9 at 0x71c620, and zero flags at 0x5d9680/84. The unresolved
question is why this device/surface configuration requests a second flip
without intervening color drawing.
Device field +0x38 is not populated by a Glide capability query. Constructor
0x466810 allocates a 0x10c-byte device and copies the caller's first
0x6c configuration bytes into it at 0x466b9e–0x466ba5. The normal MFC
creation path at 0x499450 explicitly sets configuration +0x38 to zero
at 0x49949e, then calls this constructor at 0x49950d. An alternate
path at 0x499320 sets it to one, but its entry checks 0x477a70, whose
original implementation is exactly xor eax,eax; ret; consequently that
alternate path is disabled in this executable. The constructor can also
clear a nonzero value if an existing device already has it set. The observed
zero therefore matches the original executable's selection, and changing
Glide query results or forcing this field would not be a supported fix.
The pinned Glide 3 SDK's gglide.c implementation of grBufferSwap
unconditionally cycles current/front/back indices modulo the configured
buffer count, queues the swap command, and selects the new drawing buffer.
It has no exception for an empty frame. Its fast clear also respects the
RGB write mask, so a depth-only clear correctly preserves the older menu
color. These rules agree with the observed alternating buffer contents.
One concrete difference remains: our handle_grBufferSwap does not pass
the guest's swap interval to the host; it flips and publishes immediately.
The SDK encodes interval 1 as a retrace-synchronized swap and bounds pending
swaps. Browser compositing may therefore repeatedly sample the second UI
presentation when our two swaps happen close together. Correct pacing
would preserve both requested flips; it is not evidence that the retained
menu pixels should be discarded or that pacing alone fixes gameplay.
Buffer initialization does not explain the alternation either. All four
original grSstWinOpen call sites request two color buffers and one auxiliary
buffer. The executable imports no grRenderBuffer, grGlideGetState, or
grGlideSetState; it keeps the default back-buffer target. The SDK's initial
physical buffer numbering differs from ours, but the logical front/back
rotation is equivalent. Initial parity cannot account for menu pixels retained
after the later LFB uploads.
An isolated 120-second default-CPU replay then tested an interval-1 wait
before each swap using the existing virtual-vblank scheduler. It preserved
every requested flip and left canonical WASM and production source unchanged.
build/hype-vsync-default/frame-capture.json still alternates four finite
world frames (403–406 triangles) with the same old menu. Device and main-thread
present counts agree; the run ends without errors. World visibility between
actual publications was about 32–38 ms, versus 36–47 ms in the unpaced sample.
These are diagnostic observations, not controlled performance measurements.
Pacing alone did not fix stable presentation and was not promoted to production.
2026-10-05: authenticated UI clipping starts with nonfinite inputs
The corrected owning-Worker trace armed only after the successful world swap (EIP4f1644, returns4671d4/43f631), before nested UI422260. It retained four entry483bf0/post483c35 pairs, zero observer errors, then disabled its exact owned trace at the cap. Original EXE spans and actual served module/source are pinned in the immutable run.
The first UI pair already has yA=NaN (ffc00000), yB=0; its A vertex is (635,NaN). The second enters with yA=+Infinity (7f800000), yB=NaN. Later pairs also contain infinity. Therefore this observation does not establish Y interpolation as the first corruption and does not justify patching its arithmetic. Next trace the incoming quad and earlier X clipping to locate the first nonfinite value. Preserve raw float bits; JSON null is not a sufficient numeric description.
Evidence: scratch/runs/20261005-hype-ui-clipper-nonfinite-input (207 artifact hashes rechecked); raw clipper SHA256 c0ecfa49e04bc0a296f5f7d68696aa4d21665802e9788f66cf276e9ad87a45ee. Session74195 exited0, browser/server closed10:33:12.696Z. after-trace.png shows the knight/street world, but this intermittent scene is not stable gameplay or input qualification. No FPS or normal-timing claim under trace.
UI polygon before X clipping (attempt7)
The first authenticated UI polygon is already (0,0),(+Infinity,0),(+Infinity,+Infinity),(0,+Infinity) at483670. Its raw caller return4870c9 identifies the quad path486d20. Two X entry/post pairs are captured, with no observer errors; the next swap matches the expected UI caller and ends the phase. NaN X/Y interpolation results follow the already-infinite input rather than establishing an emulator clipping defect.
Source486d51..486d71 copies XY from four16-byte input records to four60-byte staging vertices at835ae0. The input pointer is original ESP+0xc; context is original ESP+0x1c. The next minimal capture is function entry486d20 (actual caller/input64bytes) and post-copy486d73 (same pointer/staging240bytes), followed by the polygon entry to detect any intervening change. Do not assume the original source coordinates are infinite without this observation.
Immutable evidence: scratch/runs/20261005-hype-original-ui-quad-infinite,207 artifact hashes verified; rawSHA256 fa676bf6bc64403017f60ac961765e984dccee3c14f76e1bb3e8b80b59cb4ac7. Session35108 exited0, browser/server closed10:42:22.298Z. The image remains menu-only; gameplay and FPS remain unqualified.
Incoming rectangle confirmed infinite (attempt8)
The ordinary UI-phase probe captures the complete486d20 entry →486d73 copy →483670 polygon chain with zero observer errors. Actual caller469a5b authenticates the rectangle constructor469710. Incoming quad0420f988 already has (0,0),(+Infinity,0),(+Infinity,+Infinity),(0,+Infinity); all XY bits match both copied staging and later polygon. Neither copying nor clipping introduces the first infinity in this capture.
Immutable run scratch/runs/20261005-hype-incoming-quad-infinite has207 rechecked artifact hashes; rawSHA256 da1af7a6181198834030fa7630b500c529e1adeb330a555c5d2d4cbd545fbe2c. Browser7356 exited0, closed11:11:54.802Z. Menu-only image; no gameplay/FPS qualification. Next inspect actual469710 incoming rectangle, descriptor dimensions, live reciprocal table entries, and post-scaling arguments before its quad construction. Do not assume which size/angle branch executes.
Actual rectangle caller and smallest missing divisor evidence (attempt9)
Observed caller is41f17c, correcting the earlier static candidate41f352. The original rectangle arguments already contain (0,+Infinity,0,+Infinity). Live descriptor dimensions are636x476, reciprocal table words are004ccccd/00088889, and the post-scale width factor is finite0.9937500357627869. Therefore469710 size scaling is not the first infinity producer in this capture.
Original caller41f0e4 divides1 by71cdd0 (descriptor71cd74+5c), and41f125 divides1 by71cdd4 (+60), then multiplies actual dimensions and submits at41f177. Initializer4678ed/4678fa stores width/640 and height/480 in these fields after466cc0. Later466cc0 updates dimensions but does not write+5c/+60. This identifies a precise dependency, not proof that initialization was skipped or the fields are zero. Previous84-byte descriptor capture ended before them; the next approved extension reads exactly these8raw bytes at the same authenticated rectangle entry. Zero/denormal inputs can produce nonfinite results under correct arithmetic; finite-normal inputs still require exact x87 operands/precision proof before blaming CPU execution. No guest-state repair or clipping patch is justified yet.
Immutable evidence scratch/runs/20261005-hype-rectangle-caller-divisors has207 verified artifact hashes; rawSHA256 406e004704de98fa44f56bfb424cf0905944ddd4d557bf46927345754fc30624. Browser78398 exited0, closed11:24:11.451Z. Intermittent world image is not stable gameplay qualification; no FPS claim.
Zero descriptor scales observed (attempt10)
The exact eight bytes at71cdd0/71cdd4 are 0000000000000000: both descriptor scale divisors are positive zero at authenticated rectangle entry from41f17c. This directly explains the positive infinities through the original1/+0 arithmetic, without establishing a CPU division bug. The observation does not yet explain why initialization or later lifetime handling left those fields zero. Do not replace guest values or suppress swaps.
Immutable scratch/runs/20261005-hype-zero-descriptor-scales has207 verified artifact hashes; rawSHA256 471da24aff6154699b09aafcc796cf6330fb68fc482d3f3bdb9e901b312020a0. Session27505 exited0; browser/server closed11:33:01.065Z. One complete rectangle/quad chain, zero observer errors, exact trace disabled. Image remains menu; no gameplay/FPS claim.
Static lifetime distinction:404c80 registers the UI descriptor71cd74 via466de0, which allocates a separate104-byte heap surface record, copies100bytes into it, updates dimensions with466cc0, copies100bytes back and stores the heap pointer in the device surface table.467730 later initializes+5c/+60 on the heap surface, not necessarily the static UI descriptor;467010 copies100bytes from heap to caller. Thus static-versus-heap identity must be checked before calling this skipped initialization. A bounded read of the live UI surface ID71cd70 and its device/surface-table pointer at use can distinguish a stale static descriptor from zero scales in both records. No broad renderer trace is needed.
Refined next observation: authenticated acquisition copy, no pointer walk
The proposed ID/device-table lookup is superseded by the exact live copy path:4222b3 calls467070, which resolves the actual heap surface and copies100bytes to static71cd74 at4670ce on every UI acquisition. Read actual EAX source and EBX destination at4670c3, verify caller4222b8 at original stack+24, then record both100-byte records after the copy. Use known branch-entry checkpoints4670e1/467105, not an assumed post-REP block at4670d0; require same TID, ESP+8, ESI=source+100, EDI=0, ECX=0 and retained EDX/EBX identity. Compare dimensions and scales with the later authenticated rectangle-use snapshot. One acquisition/rectangle chain, three seconds or next swap, no guest mutation.29 focused tests pass.
Source ordering within467730 is dimensions-update466cc0 before scale stores4678ed/4678fa for each heap surface. Surface creation itself466de0 updates dimensions and copies descriptor fields but does not compute scales. Actual ordering between initial creation, mode changes, scale initialization and later copying is not yet observed. A zero heap source would shift diagnosis to that lifecycle; different source/postcopy/use values require their own provenance before any repair.
Heap source is already zero; descriptor copy is faithful (attempt11)
Authenticated acquisition resolves heap surface042fef50 and static UI descriptor71cd74. Before copy, heap+5c/+60 are zero; static destination is the expectedcccccccc prefill. After the100-byte copy, destination matches source exactly, including zero scales, and the rectangle-use snapshot retains those zeros. No copy defect or intervening scale overwrite is shown. One complete identity-checked chain, zero observer errors, owned trace disabled.
Immutable run scratch/runs/20261005-hype-heap-zero-scale-copy,207 artifact hashes verified, rawSHA256 92009c5b634ab403eb58e9e4dc64a46edde0c5b8e599bbe03a3bbbb3cf09ade6. Session2554 exited0 and closed11:49:16.362Z. Menu screenshot only; no gameplay/FPS qualification. Evidence files were hardlinked after closure to avoid redundant disk use; contents and paths are preserved.
Next examine creation/mode lifecycle, not rendering: source466f60/466c77/4674a5/4678c3 calls dimensions-only466cc0; only the467730 mode/reset loop then stores scale fields at4678ed/4678fa. UI surface creation466de0 can therefore leave scales copied from zero static storage until such a loop includes the new heap. Caller49977e invokes467730 conditionally when frame+54 is zero;499921 invokes it after466460. Their actual order relative to UI surface creation remains unobserved. Do not infer skipped initialization from use-time zeros alone.