Range: 71f4ce2 through 62f05de
Major Changes
On the MUSIC SELECT screen, the difficulty dots are large crisp red circles on original hardware and a few blue pixels in rePIU. Everything else on that screen — the font, the background, the four discs — looked right. Tracking down that single symptom took five instrumented runs and two wrong conclusions, and ended up fixing three unrelated defects.
The headline finding: Glide texture coordinates are not in texel units. The coordinate space spans 256 along a texture's longer axis whatever its LOD, with the shorter axis scaled by the aspect ratio. A 32×32 sprite is therefore addressed with s and t running 0..256. Normalizing by the texture's pixel size happens to be correct whenever the longer edge is already 256, and overshoots by 256 / size for anything smaller.
flowchart TD
S["Symptom: dots render as a few pixels"] --> H1["1: no-op draw paths<br/>grDrawPoint/grDrawLine"]
S --> H2["2: texture load failure<br/>chdir datas\texture fails"]
S --> H3["3: no texture bound<br/>grTexSource miss"]
S --> H4["4: textures overwritten<br/>grTexTextureMemRequired"]
S --> H5["5: coordinate normalization"]
H1 --> R1["Rejected: never called<br/>only grDrawTriangle, 8,706 times"]
H2 --> R2["Rejected: absent from the CHD<br/>normal fallback"]
H3 --> R3["Rejected: missing-sources=0"]
H4 --> R4["Real defect, wrong cause<br/>screen unchanged"]
H5 --> R5["Confirmed: st 0..256 on a 32x32 map"]
Three rounds of instrumentation that missed
The symptom admits three causes: the game never submits those quads, it submits them with no texture bound so they draw untextured, or everything is right and the pixels vanish later. Each needs a different fix, so the reading rules were pre-registered before the instrumentation went in.
The first census counted quads of 48 pixels or less and sampled forty of them:
[repiu-draw-census] after 7000 draws: small=1422 small-untextured=0 stored-textures=26 missing-sources=0
1,422 small quads, zero of them untextured, zero grTexSource misses. Two hypotheses died together. But all forty samples were title text at the top of the screen, because a first-N sample is consumed before the select screen ever appears.
The second round sampled periodically and added alpha statistics to texture downloads. Dumping the 32×32 dot sprite to a BMP showed a perfect decode — a black-outlined circle with a clean circular alpha mask, 208 opaque against 816 transparent texels. The decode-fault hypothesis died too.
The third round targeted draws bound to sub-256 textures, and a fading panel (64×256, constant color stepping F9→C6) ate the budget. The fourth targeted quads of 8 pixels or less and matched nothing at all: the game never submits any. So the dot quads were full size and their pixels were disappearing later.
Abandoning filters
All four failures shared a cause: each filter was designed without knowing what distinguishes the target. Size did not isolate the dots, texture size did not, and neither did being tiny.
So filtering was dropped for REPIU_GLIDE_FRAME_DUMP, which logs every draw of whole frames at a chosen swap interval. A frame holds only 35–100 draws, so reading all of them costs nothing. The answer appeared immediately:
[repiu-frame-dump] draw bbox=40.00x40.00 xy=(209.31,286.81)(249.31,286.81)(209.31,246.81)
st=(0.00,0.00)(256.00,0.00)(0.00,256.00) textured=1 tex=0x00062000 texdim=32x32
const=0xFE6565FE combine=3/1/1/1 blend=1/5
One frame contained 46 draws bound to a 32×32 texture (23 dots) and 8 bound to a 64×64 one (4 arrows) — exactly the two elements that looked wrong.
The 40x40 bounding box is the right size. The problem is st=(0,0)~(256,256): the game addresses a 32×32 texture with coordinates running to 256. Dividing by 32 overshoots eightfold, and with clamping the sprite covers an eighth of the quad while the rest samples the transparent edge texel. Five pixels of forty — precisely what the screen showed.
Reviving a hypothesis that had been rejected
An embarrassing record is worth keeping. This coordinate hypothesis was raised mid-investigation and rejected, on the strength of this sample:
bbox=64.0x255.9 st=(0,0)(64,0)(0,256) texdim=64x256
A 64×256 texture with s spanning 0..64 and t spanning 0..256 looked like proof that coordinates are texel-space. But 64×256 has aspect 1x4, so its extent is (64, 256) — identical to its pixel size. The sample could not distinguish the two rules at all. Rejecting a hypothesis on evidence with no discriminating power was the single largest delay here. Only a map whose longer edge is under 256 can settle it, and the 32×32 sample did so at once.
The second defect: GrColor_t format
The same log line held another answer. Read as ARGB, 0xFE6565FE is R=0x65, B=0xFE — blue. The dots on original hardware are red.
The grSstWinOpen arguments settle it:
Win32 Glide call trace: ordinal=118 name=_GRSSTWINOPEN@28 count=1
first_stack=0x030592A7 0x00000000 0x00000007 0x00000000 0x00000001 0x00000001 0x00000002 0x00000001
The fourth argument is cformat=1 = GR_COLORFORMAT_ABGR. GrColor_t byte order is chosen there, and the renderer read every value as ARGB. Under ABGR, 0xFE6565FE is R=0xFE: red.
Why this hid so long is the interesting part. Almost every constant the game sets is achromatic — 0xFFFFFFFF, 0xB2FFFFFF, 0x98000000 — and greys are unchanged by swapping red and blue. The one element using a chromatic constant was the already-broken dot.
The third defect: our output feeding back as guest input
Unrelated to the dots, the instrumentation exposed one more defect:
[repiu-tex-args] memrequired evenOdd=3 smallLod=0 largeLod=0 aspect=0 format=0 data=0x00000000 -> bytes=8192
Every field decoded as zero while the raw bytes in guest memory were correct:
raw=05 00 00 00 | 05 00 00 00 | 03 00 00 00 | 0A 00 00 00 | 00 00 00 00
smallLod=5 largeLod=5 aspect=3 format=10
grTexTextureMemRequired never read the guest structure. The pointer served only the readability check while a default-constructed value went into the calculation, so every texture answered 8192 bytes (LOD 0, aspect 8×1, one byte per texel = 256×32×1).
That matters because the function is not a passive query: the guest lays out its own TMU address space from the answer, so it was packing 128KB maps 8KB apart. The uniform 0x2000 texture spacing observed across four runs and taken for a property of the game was a function of our own output — precisely the circular trap the project's knowledge base already warned about.
Final state
| Defect | Location | Commit |
|---|---|---|
grHints unimplemented |
linexe_glide_boundary.cpp |
71f4ce2 |
GrTexInfo never read |
grTexTextureMemRequired |
659053d |
Coordinate normalization + GrColor_t |
glide_opengl_backend.cpp, glide_hle.cpp |
62f05de |
grHints was a separate item: the only unimplemented Glide API in the log at 298 calls, now implemented as per-type state recording. Because the GrVertex layout is fixed by the ABI, STWHINT cannot change the output of a renderer that reads the structure directly, so recording is a complete implementation rather than a stub. GR_HINT_FPUPRECISION is recorded but not applied: the host does not execute the guest's floating point, and changing the x87 control word would alter guest results.
The instrumentation commits are c086911, d99f849, 31285a6, e26e06f, dfa9f8f, and 05b25e7 — all environment-gated, off by default, and none of them change the rendering path.
Verification
plan_build_bench_all=true
arena_view_all=true
coherence_all=true
glide_issue_probe=pass
aot_probe_exit=0
Debug and Release builds pass, as do every repiu_aot_probe group and repiu_glide_issue_probe. On screen the dots now match the original hardware capture as large red circles, and everything drawn from 256×256 textures is unchanged.
xychart-beta
title "Hypotheses remaining per instrumented run"
x-axis ["Run 1", "Run 2", "Run 3", "Run 4", "Run 5"]
y-axis "Remaining" 0 --> 5
line [3, 2, 2, 1, 0]
Lessons
Do not reject a hypothesis on evidence that cannot discriminate. The 64×256 sample gives the same answer under both rules, and it was used to reject the correct explanation.
Do not build a filter for something you cannot yet describe. Four filters missed; dumping everything worked on the first try. When the data is small, complete observation beats selection.
Suspect the places where your output becomes the guest's input. grTexTextureMemRequired determines how the guest lays out memory, so "observed game behavior" there can be your own bug reflected back.
Achromatic evidence proves nothing about color. The screen looked correct because nearly every color constant the game sets is grey.
Technology Stack Used
Glide 2.x texture coordinate space
GrVertex.tmuvtx[].sow/tow carry s/w and t/w, where s and t are not texel coordinates. The space is set by the aspect ratio alone:
s_extent = 256 >> max(aspect - 3, 0)
t_extent = 256 >> max(3 - aspect, 0)
GrLOD_t plays no part in it; the LOD sets the texture's pixel size (longer edge = 256 >> lod).
| Texture | LOD | aspect | Pixel size | Coordinate extent |
|---|---|---|---|---|
| Font atlas | GR_LOD_256 |
1x1 |
256×256 | 256 × 256 |
| Difficulty dot | GR_LOD_32 |
1x1 |
32×32 | 256 × 256 |
| Message bar | GR_LOD_256 |
4x1 |
256×64 | 256 × 64 |
| Fading panel | GR_LOD_256 |
1x4 |
64×256 | 64 × 256 |
The implementation computes the extent from the aspect at download time, stores it with the texture, and normalizes by it when drawing.
flowchart LR
D["grTexDownloadMipMapLevel<br/>largeLod, aspect"] --> C["CalculateGlideTextureCoordinateExtent"]
C --> E["TextureEntry.s_extent / t_extent"]
D --> P["pixel size (256 >> lod)"]
P --> T["glTexImage2D"]
E --> N["glTexCoord2f(s / s_extent, t / t_extent)"]
GrColorFormat_t
The byte order of the GrColor_t taken by grConstantColorValue, grBufferClear, grFogColorValue, and grChromakeyValue is chosen by the fourth argument to grSstWinOpen.
| Value | Format | Bytes (MSB→LSB) |
|---|---|---|
| 0 | GR_COLORFORMAT_ARGB |
A R G B |
| 1 | GR_COLORFORMAT_ABGR |
A B G R |
| 2 | GR_COLORFORMAT_RGBA |
R G B A |
| 3 | GR_COLORFORMAT_BGRA |
B G R A |
PIU selects ABGR. This axis is entirely independent of GrTextureFormat_t, so textures can be correct while constant colors are inverted.
grHints
grHints(GrHint_t type, FxU32 mask) declares driver optimizations rather than rendering state.
| type | Meaning |
|---|---|
GR_HINT_STWHINT(0) |
Which w and s/t values are unique per TMU |
GR_HINT_FIFOCHECKHINT(1) |
How often Glide polls the command FIFO |
GR_HINT_FPUPRECISION(2) |
The x87 precision Glide may use internally |
GR_HINT_ALLOW_MIPMAP_DITHER(3) |
Whether mipmap dithering is allowed |
Because the GrVertex layout is fixed by the ABI, STWHINT changes what the driver sends to hardware, not where fields live, so a renderer reading the structure directly produces identical output for any value.
Observation tooling
Everything built for this investigation is opt-in through the environment.
| Variable | Purpose |
|---|---|
REPIU_GLIDE_DRAW_CENSUS |
Bucket draws by size and texture, sample them |
REPIU_GLIDE_TEX_CENSUS |
Texture download arguments and raw GrTexInfo bytes |
REPIU_DUMP_TEXTURE_BMP |
Dump decoded textures as BMP plus alpha statistics |
REPIU_GLIDE_FRAME_DUMP |
Every draw of whole frames at a chosen interval |