NFS III original Glide renderer

Status: original-renderer race/input acceptance verified in Chrome with both WebGL and native WAT software rendering.

Comparative measurements and reproducible runners are documented in the renderer benchmark report.

Use the existing nfs3_demo fixture and keep its renderer DLLs unmodified. Set HKLM\Software\Electronic Arts\Need For Speed III Demo values:

Name Type Value
Thrash Driver REG_SZ voodoo
D3D Device REG_DWORD 0

This selects voodooa.dll, SHA-256 6c7b0a1bd3ea4f7c673b7ff89db25379939171d33b1a969aa2506e44b8103b77. The alternative stem voodoo2 is not yet runtime verified. Select the renderer through the registry; the normal app default need not change.

Baseline trace, 2026-09-28

The initial trace used the main checkout's compiled WASM copied into this worktree, because that build includes the Watcom BSS initialization fix. It is external baseline evidence, not validation of the new Glide source. The original renderer loads at 0x00b30000, preferred base 0x60000000; convert a runtime address to the preferred VA by adding 0x5f4d0000. Its DllMain at runtime 0x00b3480e returns 1.

The DLL has .bss with VirtualSize 0, RawSize 0x1a00, RawPointer 0. The PE loader must zero this unbacked section instead of copying file headers.

LoadLibraryA("glide2x.dll") is followed by 58 distinct decorated GetProcAddress lookups, matching every name in the voodooa.dll static inventory at build/glide/nfs3-api-inventory.json. These lookups are executed evidence; they do not establish that the game actually calls every API. The baseline returned zero for all 58. It then called the null grGlideInit pointer at runtime 0x00b31dbd, with return address 0x00b31dc3 (batch 4). The next call is grSstQueryHardware with the hardware output at preferred VA 0x60011504; a false result exits initialization.

Repeatable trace (registry snapshot must contain the values above):

node test/run.js --app=nfs3_demo --threads --real-ticks --no-build \
  --quiet-api --quiet-blocks --stuck-after=100000 --max-batches=100000 \
  --max-seconds=40 --batch-size=10000 \
  --reg-import=build/glide/voodoo-reg.json \
  --trace-reg --trace-api=LoadLibraryA,GetProcAddress --trace-api-dedup

The CLI imports snapshots before app startup defaults. If the app later adds defaults, use an explicit launch override or the browser harness's isolated startup-registry override. Never modify the shared fixture manifest for a probe.

Acceptance

test/test-nfs3-glide-web.js uses the ordinary browser app selection and launch with workers enabled, records the fixture hash and console output, and saves screenshots and runtime counters under build/nfs3-glide/. Both WebGL and software must be exercised. Loading the native DLL and drawing its loading picture alone do not pass: inspect race/HUD output, advancing frames and accelerator input. Safari needs separate coverage from this Chromium harness.

The 2026-09-28 WebGL integration run used Chrome 153.0.8010.53 with real workers and the unmodified renderer hash above. The displayed screenshot shows the cockpit, HUD and track at 43 MPH after holding Up; the race clock advanced to 0:04.39. Geometry advanced from 109,402 to 301,213 triangles and 595 to 696 presentations, with zero renderer errors and no guest traps. Texture uploads and both LFB reads and writes ran along this route. Artifacts: build/nfs3-glide/webgl/{stats.json,race-before.png,race-after.png}.

The software run on the same browser and fixture also passed: the car moved from 0 to 61 MPH, the clock advanced to 0:05.12, and the displayed cockpit, track, car bodies and cutout trees were inspected. Geometry advanced from 102,882 to 223,710 triangles and 536 to 598 presentations, with zero renderer errors and no guest traps. All 307,200 pixels of the final raw drawable were verified opaque. Artifacts are under build/nfs3-glide/software/. These loaded-machine runs establish functionality, not comparative FPS.

Use node test/test-nfs3-glide-web.js for WebGL or GLIDE_RENDERER=software node test/test-nfs3-glide-web.js for native WAT software. The harness sets only its page's launch registry, checks the actual composited canvas for visible geometry, focuses the game and holds Up while frames advance. It fails on guest-worker traps as well as renderer errors.

Focused pixel tests passed in Chrome WebGL 1/2 and Safari 26.4 WebGL 1/2. The Safari probe verified truncated mip sampling, W-depth rejection and the published 2D presentation surface with no GL errors; its local report is build/glide/safari-results.json. This is renderer coverage, not a Safari full-game acceptance claim.

Implemented profile and boundaries

The first profile is one Glide 2 board, one TMU and 4 MiB texture memory. It includes triangles, lines, points, palettes, packed texture formats, explicit mip selection, color/alpha combiners, depth, fog, gamma, front/back swaps and RGB565 LFB synchronization. Glide 3, a second TMU, NCC/YIQ textures, antialiased primitives, auxiliary LFB access and compare-to-bias depth modes remain unsupported. Unsupported operations fail explicitly. The WebGL color targets use RGB8 rather than exact Voodoo RGB565 quantization and dithering.

Two integration prerequisites were exposed by the original renderer: its Watcom zero-raw-pointer BSS must be zero initialized, and a worker must adopt newly published API/continuation thunks within its current execution slice. The same-slice thunk regression reproduces the original unreachable trap when the refresh is disabled. GPU-only windows also need the compositor's ordinary backing surface before their presentation layer can be displayed. Each completed frame schedules a compositor repaint. Opening Glide publishes the selected fullscreen mode and window/client dimensions; close or shutdown restores the previous mode and window rectangle exactly once, including when another guest thread closes the board.

WAT software lowering

lib/glide-software.js consumes the same owned packets as WebGL, lowers color and alpha combiners to normalized PS1.4 IR, and uses D3D9SoftwareBackend.Device for native vertex processing, interpolation, texture filtering, depth, coverage and pixel shading. JavaScript converts textures, packs immutable draws and presents BGRA pixels; it contains no triangle rasterizer. Adjacent equal-state triangles share a native draw, up to the native descriptor's vertex bound. Texture generations retire on upload/table mutation, and resident native texture storage is reused between draws. Front and back colors have distinct identities and share one depth surface. Ordinary swaps and front-buffer writes publish complete frames. Depth uses an explicit shared D16 attachment. Coordinates are shifted by half a pixel when entering the native rasterizer, whose sample centers are integer based. Independent edge-coverage, color-interpolation and near-equal-depth fixtures distinguish these choices from the previous float-depth/integer-center behavior. A captured 189-vertex race packet matches WebGL RGB exactly after the center conversion.

The presented RGB565 display is always opaque; render-target alpha remains available for blending. A zero-alpha red draw therefore displays opaque red while retaining zero alpha in native storage and 0xf800 through LFB readback. Passing render-target alpha into Canvas2D incorrectly hid car panels and cutout-tree pixels even when their computed RGB values were correct.

The opt-in d3d_software_bind_glide descriptor attaches a copied 64-entry fog table to a native draw. Global reciprocal W travels in texture coordinate Z; TMU reciprocal W travels independently in coordinate W for projected sampling. The rasterizer encodes global W after interpolation, including signed 16-bit depth bias, then performs the existing depth test. Table fog uses that encoding, independently of the selected Z/W depth-buffer mode. The helper matches the exponent/mantissa formulation of MAME's Voodoo compute_wfloat: reciprocals 1, .875, .75, .625, .5, .25, 0 encode to 0, 1024, 2048, 3072, 4096, 8192, 65535.

test/test-glide-software.js checks independent expected colors and depth values for Z/W occlusion, perspective inputs, table fog, palette replacement, chroma rejection, front/back LFB and native-rendered lines/points. The shared test/test-d3d-software-pipeline.js regression passes 375 native pipeline cases. The software line path expands one-pixel rectangular geometry; matching the hardware's diamond-exit edge rule remains a documented fidelity limitation. Separate RGB/alpha clear masks, two-TMU detail/LOD combiners and unsupported fog modifier flags are explicit errors rather than accepted approximations.

Original Glide snapped coordinates

The first native Glide browser run reached _grDrawLine@8 with vertex x bits 0x494024dd, or 787021.8125 = 786432 + 589.8125. This is an original Glide coordinate encoding, rather than a corrupted vertex or an NFS-specific offset. In pinned 3dfx source revision 2f226f0f9225ce8ee83e6a4a7042981e719d19ee, glide2x/sst1/glide/src/gsplash.c defines SNAP_BIAS as 3 << 18, adds it directly to projected GrVertex.x/y, and calls grDrawTriangle without removing it. The same source tree's gdraw.c explicitly accepts already-biased point coordinates. No hint enables this path.

The 3dfx programming guide documents signed 12.4 screen coordinates and the 3 << 18 snapping technique. The frontend decodes precisely the alternate signed coordinate interval [786432 - 2048, 786432 + 2048) by subtracting 786432 from copied x/y only. Ordinary coordinates and values outside that interval remain unchanged. This includes biased negative coordinates and preserves the existing 1/16-pixel precision. All primitive packets share the conversion, so GPU and software receive the same screen coordinates and guest vertex arrays remain untouched. test/test-glide-abi.js checks both interval boundaries, ordinary positive and negative coordinates, and the observed NFS bit pattern.