These pages are generated from the project's internal documentation. References to source files and binary offsets are kept as provenance — so you can see where each claim comes from — but they are not followable yet: the engine is not published. Once it is, they will become links.
1 · Image improvements
Everything that changes what you see: the smoothing, the two skins, sprite and tile transparency, typography, and lines and outlines.
Tags: 🎯 FIDELITY (the "before" was our own bug) · ✨ ADDED (the original did not do it) · ⚖️ DECLARED DIVERGENCE · 🗑️ REMOVED. See the overview.
1 · The two skins: "1988" and "smooth"
✨ ADDED · Code: game/src/skin/fiel/skin.ts, game/src/skin/shader/skin.ts, game/src/skin/manager.ts · Switched live with F9 or from ⚙ → Video.
Before
The original has a single presentation: 16-colour EGA, 320×200, fat pixels. There is nothing to choose.
Now
There are two skins, interchangeable at runtime, and one is a replica of the other:
- "1988 (faithful)" — the 1988 frontend rebuilt piece by piece from the binary: EGA chrome with literal coordinates (
paint_screen_frame@0x637e), an 11×11 viewport with the EGA tile pack, roster and console in theIBM.CHfont. - "smooth" (shader) — the same faithful skin with ONE pipeline change, not a reimplementation. It instantiates a
FaithfulSkinthat paints to a hidden 320×200 canvas, and composites each frame in two steps: the whole faithful frame is scaled by nearest neighbour (chrome and text byte-identical to the faithful skin), and only the viewport rectangle (176×176) goes through the edge filter.
Why it matters
That the shader hosts the faithful skin instead of duplicating it is the decision holding up everything else: the animation clock, the combat overlays, the moongate, the earthquake, the Ztats modal and the audio remain the faithful skin's. When a defect is fixed in the faithful skin, the shader inherits it without a line being touched. The 1988 replica never forks.
And filtering only the viewport is not laziness: text and chrome live outside that rectangle, so the filter cannot touch them. An upscaler run over the 8×8 font would have turned it to mush.
Trade-off
- The intro cinematic (ui/faithful-intro.ts) is a separate subsystem that runs and finishes before any skin is mounted: its plates do not go through the filter. It is declared as a boundary in the docstring of skin/shader/skin.ts.
- The overworld gem uses a separate DOM panel and is not filtered either; the dungeon gem, which the faithful skin does paint inside the viewport, is filtered. Two gems, two behaviours.
2 · The smoothing: xBR on the GPU
✨ ADDED · Code: game/src/skin/shader/xbr-gl.ts, upscaler.ts
Before
Nearest neighbour. One pixel of the original is an N×N square of screen pixels. It is what the "1988" skin does, and it is the right thing for it.
Now
The viewport goes through xBR level 1 with Hyllian's edge-detection rule (weighted difference in YUV, weights 48/7/6), ported to GLSL and run on WebGL1. The pipeline chains two 2× passes (= 4× internally) and leaves the 4→6 stretch to the bilinear blit.
Why it matters
An edge filter does not "blur": it reconstructs the diagonals that the 16-pixel grid could not represent. Measurement over the split layout gave 12.8× more distinct colours in the viewport than the faithful skin (2026-07-28) — the filter is working, not decorating.
Trade-offs, and there are several
- Licence: the kernel is the project's own port of extractor/src/upscale/xbr.ts, not the GPL
.glslfrom RetroArch that the spec recommended. The deviation is declared in the header ofxbr-gl.tsand was reported to the publication lane. - No WebGL, no filter: there is a
NearestUpscalerfallback that scales to 6× by nearest neighbour. The shader skin stays mounted and switchable, but it looks identical to the faithful one. It is a silent degradation: nothing warns the player. - What gets cut out loses the smoothing. Sprites recomposed from the EGA atlas (§3 and §4) are blitted with nearest, so the cut-out sprite does not carry xBR. It is recorded as an inherited caveat, not as a regression.
- Collapsed backbuffer if the host measures 0×0. Hosted inside another layout with no size, the shader computes
ceil(320/320) = 1and stays at 320×200: no smoothing, no error. Diagnosed and resolved later. It is the most treacherous failure mode of all, because the game works perfectly and the only thing wrong is the single reason anyone chose this skin.
3 · Transparency I — the body at alpha .55
✨ ADDED · Shader skin · Kill switch ?transp=off
Before
Nothing in the port applied alpha < 1 to the body of a tile or an actor. Actors already had their black background cut out, but the body was solid: a ghost hid the floor just like a troll.
The census first, the wiring after
Before a pixel was touched, a census of candidates was made over the 529 tiles of TileData.json, with impact / coherence / cost and — the column that decides — whether the original reference already hints at it (DOS) or it is purely aesthetic (Licence). The A/B mock-up was rendered from the real HD atlas before choosing:
![]()
The user chose column B (body at alpha .55), and that is what is wired.
Now — first wave
| Family | Tiles | Route |
|---|---|---|
| Force fields (Poison/Magic/Fire/Electric) | 488-491 | terrain: synthesised floor + body at .55 |
| Ghosts | 412-415 | actor: ActorTransparency.bodyAlpha |
| Wisps | 468-471 | actor |
| Shadowlords | 508-511 | actor |
And in the second wave, the shadowlord boundary (112-127, as "mist") and the blue flame (222).
🗑️ What this part used to show, and why it no longer does
There was an A/B comparison of the translucent moongate here. It has been removed, because the effect no longer exists.
The moongate was wired in the first wave and the user ordered it taken out on 2026-07-22 ("the moongate must not have transparency inside", with two screenshots). It is in the code, at the exact spot where it used to be painted:
// MOONGATE: RETIRADA del censo de translucidez por VEREDICTO DEL USUARIO
The gate keeps the opaque painting of the faithful layer. Tile 220 is still in isCensusTerrainTile, but only to exclude it as a floor donor for neighbouring fields.
How it was caught, which is the reusable part: by capturing both branches of the A/B — default and ?transp=off — and comparing the sha. They came out byte-identical. A kill switch that does not change a single byte means the thing it switches off is gone. Reading the code was not needed to suspect it; only to confirm it.
The original screenshots are still in their verdict folder: they are true as of their date, and they are the record of a work lane. What cannot be done is cite them in the present tense.
Why it matters
It is the canonical use of translucency: a ghost that looks solid is a ghost that is not frightening. But the relevant decision is not that one, it is the one next to it —
Trade-offs and rejections
- A whitelist by tileId, never global. An alpha applied to every actor would turn an orc translucent and break the readability of combat. It is written as cross-cutting risk #4 of the census, and the wiring respects it.
- Before the fog, not after. Fields and flames live in interiors where the
fogCanvasalready modulates; an alpha applied afterwards would multiply with the fog and the sprite would vanish at night. The whole pass goes before. - Windows were rejected. Alpha .55 was tried on the window tiles (74/75) and the result only dims the stone: it reveals nothing (the background is black) and does not produce the "luminous glass" that was wanted. What the census asked for — interior glow at night — is a glow, i.e. new design. The scaffolding was removed.
- Water is not treated, on purpose.
WaterStream(96-111) andCornerWithWater(228-231) are already composited by thewaterfn32layer; adding alpha on top would be double treatment and, worse, would reveal the void behind (a river has no floor beneath it). The waterfall (212-215) is indeed left uncovered, but its "dominant neighbour" is more waterfall or rock: the synthesised floor would come out wrong. It stays as a future lane, not an oversight. - A 16-colour floor: below roughly alpha .45 over dark terrain the tile becomes illegible. The .55 chosen sits just above that floor.
4 · Transparency II — the outline cut-out
✨ ADDED · Shader skin · Kill switch ?contourTransp=off
This is the other treatment, and it is not the same as the previous one. Here the body's alpha is not lowered: the outer black pixels of the sprite are replaced by transparency (the ones connected to the cell edge, by flood fill), and the inner blacks are kept. The sprite stays opaque; what disappears is its background square.
Before / after — the fountain in Lord British's castle
?contourTransp=off | default |
|---|---|
![]() | ![]() |
On the left, the grid floor is cut off in black at the corners of the tile. On the right, the grid continues through the corners and only the water and the basin remain.
The underlying problem, and its solution
The fountain is baked by the faithful skin with no per-tile hook: there is nowhere to attach. The way out was to synthesise the floor underneath with dominantFloorNeighbor — of the four orthogonal neighbours, the floor tile that appears most, copying its already-rendered (xBR-exact) pixels before blitting the cut-out.
The regression that appeared, and why it is instructive
On wiring the cut-out, the fountain stopped animating. terrainWindow carries the raw map tile (fixed base frame, 0xd8); the faithful skin animates the fountain at draw time, with animatedFrame(tile, phase, groups). The cut-out blitted the raw tile, so it covered the animated fountain of the world layer with a static frame. A visual fix that freezes what was alive.
The repair: the cut-out derives the live frame using the same phase as the faithful skin (FaithfulSkin.animPhase). The four frames, cycling with the cut-out active:

And a measured regression guard was left behind (SAD between distinct frames = 133 454, i.e. ≠ 0) in game/tests/contour-transp.test.ts.
Extension to the fires, which do not animate the same way
Braziers (0xb2), torches (0xb0/0xb1), bonfires (0xb3), hearths (0xbc–0xbf) and the blue flame (0xde) do not cycle their id: their flicker is procedural fn32 noise on a live canvas. The cut-out treats them differently — it synthesises the floor, and then cuts the live flame canvas by the tile's static silhouette (destination-in), so the flicker survives and the background is removed.
?contourTransp=off | default |
|---|---|
![]() | ![]() |
What is NOT touched: its contribution to lighting (lightLevel / visMask). Only the painting. A presentation fix that had moved the lighting would have changed the game.
Trade-offs
- It is only legible over dense floors (brick, grating). Over grass or EGA desert — which are mostly black — the cut-out is invisible. A finding of the lane itself: the effect exists everywhere and shows in few places.
- The cut-out is blitted from the EGA atlas by nearest ⇒ the cut-out sprite loses the xBR smoothing. See §2, trade-off 3.
5 · The rule that cost two user screenshots: "never a wall as background"
✨ ADDED (correction to an addition) · Code: game/src/render/floor-underlay.ts
The "dominant neighbour" of §4 had a defect you only see by looking: if the cell's neighbours were walls, it synthesised wall as floor, and the cut-out painted brick where the player sees stone.
Two rules, each with a visual witness from the user behind it:
- Walls are not candidates for floor.
isFloorUnderlayCandidate: a candidate is floor if it is walkable on foot or navigable (grass, brick, wood, carpet, bridge, stairs, lava, water). Walls, dry stone, doors and windows are out. With three walls and one floor around, the floor wins: the walls do not even count. All walls ⇒null⇒ cell untouched. Never a wall as background. - Wall-mounted fires are not cut out. The left/right sconces (0xb0/0xb1) and the hearth (0xbc) already carry the wall in their graphic; cutting them out painted brick behind. They are left with their whole cell baked, and the faithful skin keeps supplying the flicker. Floor fires (brazier 0xb2, bonfire 0xb3, lamp post 0xbd, blue flame 0xde) are cut out. The ambiguous ones (0xbe candle on a table, 0xbf cooking fire) were excluded on the same principle: the background must be what is actually behind.
Verification — inside the keep of "Calma"
?contourTransp=off | with the fix |
|---|---|
![]() | ![]() |
The brazier blends with the brick (not with the white stone of the border) and the sconces stay identical between the two screenshots, because they are excluded from the cut-out.
Why it matters beyond its own lane
dominantFloorNeighbor is a shared helper used by the fountain, braziers, fields, moongate and boundary. Fixing it here also corrected what had already landed from the previous lane. A bug in a shared function has no single owner: it has all of them.
Trade-off: the improvement is deliberately UNEVEN
The price of rule 2 is that two adjacent fires look different. In a room with a floor brazier and a wall sconce, the brazier blends with the brick and the sconce keeps its baked square. To someone who does not know why, it looks as if the effect "fails sometimes".
It was accepted knowingly, and it is the right choice: the alternative — cutting out the wall fires too — was worse and also false, because it painted brick where the player sees stone. Between a uniform effect that lies and an uneven one that tells the truth about what is behind, the second was chosen. When the real background is the wall, the correct answer is not to cut out.
6 · Tiles: the corpse that was desert
🎯 FIDELITY · Code: game/src/skin/coreview.ts (arenaLoot)
Before
On the cell of a troll just killed on a bridge, no corpse appeared but "a mix of grass with transparency": green and yellow flecks over the planks.
The cause
COMBAT:0x1574 stores in the arena's object table the low byte of the remains: corpse 0x1E, blood 0x1F, chest 0x01. The renderer blits them from the high bank, with +0x100. arenaLoot painted the raw byte, without that offset — so out came their low-bank twins:
| byte | low bank (the bug) | high bank (the faithful one) |
|---|---|---|
| 0x1E | LeftDesert2 (desert) | 0x11E DeadBody (corpse) |
| 0x1F | RightDesert2 (desert) | 0x11F Splat (blood) |
| 0x01 | Water1 (water) | 0x101 Chest (chest) |
Desert tiles — green flecks on black — painted as an entity with a transparent background over the planks. Exactly what the user described.
Before / after

Top row, the low bank (the bug); bottom row, the high bank.
Why it matters
It is the kind of defect that does not look like a defect: it looks like a strange effect. The witness did not say "the corpse is missing", they said "a desert with transparency comes up" — and that is precisely the symptom a lost 0x100 offset produces. Without the user's literal description, the search would have started in the wrong place.
Trade-off: the fix is three bytes, not the family
lootTiles() keeps its low-byte contract because the tests and open_chest consume it: the +0x100 is added only at the painting site. That is right — changing the contract would have broken the other consumers — but it leaves the asymmetry alive: there are two bank conventions in play and you have to know which one you are in.
And the fix does not cover the whole family: the gargoyle (0x4C) is still deferred, because it is another layer (map terrain, not an object in the arena table) and is not solved by the same offset. It is recorded as deferred, not as resolved.
7 · Typography I — the equipment icons came from the wrong font
🎯 FIDELITY · Code: game/src/skin/fiel/skin.ts (drawReadyPicker)
The report
"the font icons in the template are NOT the game's, we need to look at that properly, we have some error"
What was NOT broken (ruled out byte by byte)
Before touching anything, the obvious was checked, and the obvious was fine:
font-ibm.png==IBM.CH: pixel-by-pixel comparison of the 128 glyphs, including the whole control range 0x00–0x1F ⇒ 0 pixels of difference.font-runes.png==RUNES.CH: same ⇒ 0 pixels.READY_GLYPH_TABLE==DATA.OVL: correct. (The comment citedDS 0x1ae8; the real offset is0x1af8by the DGROUP rulefileoff = DS + 0x10. At0x1ae8lives another table — armour values.)
The real bug
The original prints each equipped item's class glyph as an attribute character from the runic font, not from IBM. The control band of RUNES.CH carries equipment dingbats (helmet, shield, cuirass, weapons, ring); the one in IBM.CH carries triangles and box pieces. The port wrote the code into a cell that the skin rasterises with IBM ⇒ all 48/48 items rendered a different glyph from the one DOS shows.
The corollary confirms it: 0x09 (Thrwng Axe) is an empty glyph in IBM.CH — hence the "BLANK!" that had puzzled an earlier lane — but has a real glyph in RUNES.CH. No code in the table is empty in the correct font.
And what stays in IBM, verified
The picker's scroll arrows (0x18/0x19/0x12) really are IBM in real DOS: in the reference it is a clean ↓ = IBM.CH[0x19]. The scroll frame and the row text too. The fix uses the highlightCols that layoutReadyPicker was already computing and ignoring, to draw only the class-glyph cells from the runic atlas.
Trade-off: under the smooth skin these glyphs are not smoothed
The shader skin inherits the fix through the nearest blit of the faithful base, not by repainting it. That is: the equipment icons come out correct, but unvectorised — unlike the scroll arrows of §11, which are repainted vectorially. On the same screen a smooth element and a hard-pixel one coexist.
It is the cheap choice and a defensible one (a hand-vectorised 8×8 dingbat for 30 codes is a lot of work for a tiny icon), but it is a real unevenness of the smooth skin, not a coincidence.
🔴 A note on the state of the material
The record cited an image (red column = IBM, green = RUNES) that was never committed: the folder contains only the record itself. The adjudication does not depend on it — it rests on the byte-level comparison above — but the reference was broken. It is now annotated in the record itself (2026-08-04) so nobody looks for it again.
8 · Typography II — the body of signs: Latin script or runes
🎯 FIDELITY · Code: game/src/skin/fiel/sign-box.ts (layoutSignBox, runicBodyCells)
| Before (Latin) | Now, by default (runes) |
|---|---|
![]() | ![]() |
Real DOS paints sign bodies in runes, with the digraphs collapsed (TH=0x5b, EA=0x5e, ST=0x5f, NG=0x5d, EE=0x5c) — which is why "NORTH", "EAST" and "TRINSIC" come out more compact than in Latin script. The port used to do it in Latin; now it does it in runes.
It was decided in favour of fidelity (verdict #25): layoutSignBox carries const runicBody = opts?.runicBody !== false — that is, runic unless you opt out, and the Latin form survives only for the tests of the legible variant.
What saves legibility
The console log keeps the Latin script. The box is faithful in runes and the text is still readable in the history. Maximum fidelity where you look, legibility where you read — which is why the decision cost nothing that hurt.
A method detail worth keeping: the box is sized to the already composed body and capped to the panel width (interior 14, derived from SIGNS.DAT). Since runes collapse digraphs, the same text takes less room: "PRIVATE ISLAND" fills the panel exactly, with no air.
🔴 This section said the opposite until 2026-08-04. It claimed the runic body was an "unlanded mock-up" behind a
?signRunic=1flag, and that the port "stays in Latin script by default". Both were false: the flag does not exist in the tree and runic is the default. It was not a stale figure — it was the inverted outcome of a decision that was actually taken. The census of this document against the code exposed it.
9 · Typography III — the panel font, two rejected mock-ups
Mock-ups, not landed
The idea of a "transparent font" for the panel under the shader skin allowed two readings, and both were mocked up as reversible flags before deciding:
| Today's state | ?mockFont=nobg | ?mockFont=alpha |
|---|---|---|
![]() | ![]() | ![]() |
- A — today: opaque white HD glyphs on a flat black panel.
- B — the glyph loses its cell background and "floats" over a themed panel. Note the nuance from the record: over today's flat black panel, removing the cell background would not show; that is why the mock-up comes with a blue gradient. What is being shown is not just "remove the background", it is "remove the background and give the panel colour".
- C — glyphs at alpha 0.55 with soft edges over the current black panel.
Switched off, the render is byte-identical to today's (verified no-op). The record notes that in C the wind band ("Calm Winds") is painted by another route and stays opaque in the mock-up: if this path were chosen, it would have to be unified when wiring it for real.
10 · Lines and outlines I — the scroll band brackets
🎯 FIDELITY
When a Ready picker list overflows its 7 visible rows, the original paints a ► arrow ◄ band below the scroll. In the port, the brackets were separated from the blue band by a black line of 1 logical pixel.
| before | after |
|---|---|
![]() | ![]() |
And the ×8 proof of the joint, which is where it is actually adjudicated:

Two different causes for the same symptom
This is what makes the entry interesting: the same defect had a different cause in each skin, and fixing one did not fix the other.
- Shader (vectorial): the back of the chevron fell 1 logical pixel inside the cell. Fix: shift each bracket 1 px towards its band (
nudge = s). - Faithful (bitmap):
drawReadyPickerblacked out the three cells of the indicator before tracing the brackets, erasing the two base columns — the blue the chrome contributes. Fix: black out only the centre cell, the one with the arrow. EachdrawBandBracketalready blacks out its own inner notch and respects the base column.
The faithful skin's result is byte-identical to the DOS reference crop.
11 · Lines and outlines II — the arrow that was a split diamond
🎯 FIDELITY · Code: game/src/skin/shader/skyband.ts
The shader skin repaints the chrome vectorially, and its scroll arrow drew filled triangles ▲/▼. For the "there is more above and below" state (↕) it painted two triangles with a gap between them, which read as a split diamond.
| before / after, faithful skin | before / after, shader skin |
|---|---|
![]() | ![]() |
The DOS glyph is not a triangle: it is a thin arrow with a shaft (↑ 0x18 / ↓ 0x19 / ↕ 0x12 of IBM.CH). The fix vectorises that shape by measuring the real bitmap of font-ibm.png — head of half-width 3 (cols 1..7), shaft of half-width 1 (cols 3..5) — and the ↕ becomes one continuous silhouette of a double point joined by the shaft, not two pieces.
The guard was written in the term that matters: the test shader-skyband.test.ts asserts that the ↕ is 1 fill + 1 stroke, not 2+2. A test that counts drawing operations, because the defect was precisely that there were two pieces where there should be one.
The census that followed
The same indicator was missing from the Ztats lists, and there the finding was one of method: the band is not drawn by each list, it is a single kernel (0x6c0a) with no coordinates — i.e. a band fixed on screen — that several commands invoke. The derivations had been done per command, vertically; nobody swept the shared consumer. Ready implemented it when its turn came and there it stayed.
Of the three list scroll bands in the binary, the port covers two. The third is the blacksmith's wares list, and it does not apply: the port serves shops as a console flow, not as the original's framed scroll. That is a recorded presentation divergence, not a missing arrow.
🔴 And there is a fourth place where that band appears without belonging there
In the port, the Ready picker has one more client: the Mix reagent selector, which reuses drawReadyPicker with variant:"mix". So the fix above reached it for free. And with it, something that should not have:

7 reagents and a ↓. The game has 8: Mandrake Root is missing.
In the binary, the Mix reagent panel is not even the Ready picker: it is a routine of its own in another overlay, CMDS.OVL @0x18be, and it paginates nothing.
- It opens a 9-row text window —
set_text_window(1, 0x18, 1, 0x26, 9)at0x1900— for 8 items at most. It never overflows, so it never needs to paginate. - Its print loop (
0x1932-0x196d) closes withcmp si, word ptr [bp - 0x14], and[bp-0x14]is the number of reagents OWNED, counted at0x18ca-0x18dd. There is nocmp si,7anywhere. - And in all of
CMDS.OVLthere is not a single call resolving to the band kernel (0x6c0a). Zero. The original draws no scroll arrow here.
The port's 7 comes from skin/fiel/ready.ts:56 — READY_VISIBLE_ROWS, derived from Ready's window (draw_list_frame(8) @ZSTATS 0x12ee) and applied to Mix purely by sharing a renderer.
The same band is the FIX in Ztats and Ready, and the DEFECT in Mix. And the mechanism is not "a constant was inherited": it is that the port made Mix a client of the Ready picker, and the binary keeps them in two separate overlays. Reusing the renderer carried its geometry along — the page cap, and the band that announces that cap — into a case that has neither. A shared widget does not share only the drawing: it shares its assumptions, and nobody decides whether those hold for the new client.
Still open: Mix's window is 9 rows, so with 8 rows of content it has to be repositioned, and the renderer is Ready's — blast radius. Detail in docs/verdicts/mix-flow/README.md.
Corollary, and it qualifies the sentence above: of the three list bands in the binary the port covers two and the third does not apply. But "covered" is not "faithful" — and there is a fourth place, Mix, where the port paints a band that the binary does not paint. The census counted the bands that were MISSING; nobody counted the ones that are IN EXCESS.
| state | screenshot |
|---|---|
| 7 items (just fits) — no arrow | ![]() |
| 8 items, at the top — only ▼ | ![]() |
| 8 items, scrolled — ▲ | ![]() |
| shader skin — vectorial | ![]() |
12 · The transposed zodiac
🎯 FIDELITY
The zodiac plate (the starry sky with the signs) came out with the stars in the wrong positions:
| before | after |
|---|---|
![]() | ![]() |
And the zoomed detail of one sign, which is where the difference in shape shows:
| before | after |
|---|---|
![]() | ![]() |
The folder has no record, so it was measured
That folder carries no write-up. All there was, was a file name asserting "transposed", and a file name is not a derivation. Rather than repeat it as if it were one, the hypothesis was tested against the two images (2026-08-04).
Method: both plates are 528×528 and ~98% black, so comparing pixel by pixel gives 99% agreement with any transformation — the background dominates. Only the lit pixels are compared, by intersection over union (IoU), against the eight symmetries of the square:
Hypothesis: AFTER = … of BEFORE | IoU |
|---|---|
| transposed (aᵀ) | 0.4897 |
| identity (no change) | 0.0242 |
| 90° rotation | 0.0160 |
| 180° rotation | 0.0059 |
| vertical mirror | 0.0040 |
| 270° rotation | 0.0020 |
| horizontal mirror | 0.0000 |
The transposition wins by 20× over the next candidate. The file name's hypothesis is corroborated: what was wrong was swapping the two indices when reading the plate.
What this measurement does NOT establish
The IoU is 0.49, not 1.0, and that has to be said: the transposition explains the bulk of the difference, but not all of it. AFTER has more lit pixels than BEFORE (2 394 against 2 178), so the fix was not a pure transposition — there was something else, or the star sprites are not symmetric and transposing them does not map them exactly.
Which of the two, I do not know and will not invent: it would take the lane's code. What remains here is a measured hypothesis with its residual declared, which is rather more than there was.
What this document does not cover
- Fog, night occlusion and city shadows — they are painted by the faithful skin and are replica, not improvement.
- Pre-baked xBRZ water (
water-xbrz.png) — mentioned in passing in the transparency census as an existing layer; it has no verdict folder of its own with an A/B. - The 3D dungeons and their decoration — their own lane.






















