[Rendering] Investigate renderer parity: SFML (desktop) vs SDL2/WebGL (web) — shaders & pixel-perfect output #347

Open
opened 2026-07-10 22:50:41 +00:00 by john · 1 comment
Owner

Motivation

The desktop backend is SFML; the web backend is SDL2 + OpenGL ES 2 (src/platform/SDL2Renderer.cpp, SDL2Types.h). Game code is written against the SFML API and the SDL2 types mirror it, but rendering fidelity parity is not established. Until it is, shipping a game to web is dependent on careful manual testing of that game's content in both modes — we can't assume desktop-correct rendering is web-correct.

The existing CLAUDE.md "Web Build Constraints" table covers Audio / ImGui / Dynamic assets / Threading / Input, but says nothing about shader availability or pixel-perfect output. This issue is to investigate and document those gaps, then spin out concrete follow-up issues per confirmed defect.

Areas to investigate

1. Shader GLSL dialect portability. There is a real GL ES 2 shader impl on web (SDL2Types.h:1375, Shader::loadFromMemory → program compile). But nothing appears to reconcile desktop GLSL with WebGL GLSL ES 1.00:

  • precision qualifiers (required in ES, absent in desktop)
  • #version directives
  • varying/attribute (ES 1.00) vs in/out (desktop)
  • A user fragment shader (mcrfpy Shader, PyShader.cpp) that compiles on desktop may fail on web or vice versa. Determine whether we translate, require ES-compatible source, or fail silently.

2. Shader feature availability. The Shader::Type enum includes Geometry — geometry shaders do not exist in GL ES 2. Enumerate which shader features/uniform types are desktop-only.

3. Pixel-perfect output. Compare desktop SFML vs WebGL for:

  • RenderTexture / framebuffer round-tripping (UIGrid uses render-texture chunk caching)
  • Integer scaling / view centering rounding (grid.center, center_camera, zoom)
  • Text rendering (FreeType on both per CLAUDE.md, but AA/subpixel/hinting differences)
  • Off-by-one / half-pixel sampling differences

4. Headless testing gap. Headless Shader::loadFromMemory returns false (HeadlessTypes.h:931) — shader-dependent paths cannot be validated on headless CI. Decide how shader correctness gets tested cross-backend (screenshot comparison harness?).

Deliverable

An investigation write-up (wiki + expanded CLAUDE.md constraints table) documenting confirmed parity gaps, plus granular follow-up issues (system:rendering) for each fixable defect. This is a blocker to trusting the desktop→web transition for any shader-using or pixel-sensitive game.

Related: web profiling docs (sibling), Emscripten/SDL2 backend notes in CLAUDE.md.

## Motivation The desktop backend is SFML; the web backend is SDL2 + OpenGL ES 2 (`src/platform/SDL2Renderer.cpp`, `SDL2Types.h`). Game code is written against the SFML API and the SDL2 types mirror it, but **rendering *fidelity* parity is not established**. Until it is, shipping a game to web is dependent on careful manual testing of that game's content in *both* modes — we can't assume desktop-correct rendering is web-correct. The existing CLAUDE.md "Web Build Constraints" table covers Audio / ImGui / Dynamic assets / Threading / Input, but **says nothing about shader availability or pixel-perfect output**. This issue is to investigate and document those gaps, then spin out concrete follow-up issues per confirmed defect. ## Areas to investigate **1. Shader GLSL dialect portability.** There is a real GL ES 2 shader impl on web (`SDL2Types.h:1375`, `Shader::loadFromMemory` → program compile). But nothing appears to reconcile desktop GLSL with **WebGL GLSL ES 1.00**: - `precision` qualifiers (required in ES, absent in desktop) - `#version` directives - `varying`/`attribute` (ES 1.00) vs `in`/`out` (desktop) - A user fragment shader (`mcrfpy` `Shader`, `PyShader.cpp`) that compiles on desktop may fail on web or vice versa. Determine whether we translate, require ES-compatible source, or fail silently. **2. Shader feature availability.** The `Shader::Type` enum includes `Geometry` — geometry shaders **do not exist in GL ES 2**. Enumerate which shader features/uniform types are desktop-only. **3. Pixel-perfect output.** Compare desktop SFML vs WebGL for: - RenderTexture / framebuffer round-tripping (`UIGrid` uses render-texture chunk caching) - Integer scaling / view centering rounding (`grid.center`, `center_camera`, zoom) - Text rendering (FreeType on both per CLAUDE.md, but AA/subpixel/hinting differences) - Off-by-one / half-pixel sampling differences **4. Headless testing gap.** Headless `Shader::loadFromMemory` returns `false` (`HeadlessTypes.h:931`) — shader-dependent paths cannot be validated on headless CI. Decide how shader correctness gets tested cross-backend (screenshot comparison harness?). ## Deliverable An investigation write-up (wiki + expanded CLAUDE.md constraints table) documenting confirmed parity gaps, plus granular follow-up issues (`system:rendering`) for each fixable defect. This is a **blocker to trusting the desktop→web transition** for any shader-using or pixel-sensitive game. Related: web profiling docs (sibling), Emscripten/SDL2 backend notes in CLAUDE.md.
Author
Owner

First divergences confirmed by the #388 harness

The #388 cross-backend screenshot-diff harness now renders the snippet corpus on web (SDL2/WebGL) and diffs against the desktop-headless oracle. On a calibration run over snippets 1–15 (make test-wasm-diff SNIPPETS="1 6 8"), clean static frames match to ≤2.2% pixel diff, and two snippets flagged real divergences:

1. Frame outline geometry (008_frame_outline, 8.5% diff) — SDL2-specific bug.
The divergence mask (snippet-shots/web-diff/008_frame_outline_mask.png) shows the frame borders differ, and the difference scales with outline thickness (frames at thickness 0/2/5/10/20 — the thickness-20 frame is grossly wrong, ~solid mismatch). Root cause is in Shape::draw (SDL2Renderer.cpp): the outline is built as per-edge quads extruded a full outlineThickness outward along each edge normal, with no corner miter/join and a different extrusion basis than sf::Shape::setOutlineThickness. Thin outlines are close; thick ones diverge badly. This is a concrete SDL2 outline-geometry defect → belongs in #390.

2. Text rendering (labels in the same frame differ) — the §3d divergence, confirmed.
The outline snippet's caption labels also show as differing pixels, corroborating the no-kerning / byte-wise-glyph text divergence catalogued in §3d. Already routed to #390.

The harness passes clean frames and isolates genuine divergences with per-pixel masks for triage — exactly the CI signal this investigation called for. Full-corpus runs will surface the rest; each new divergence gets a mask and a line here / on #390.

## First divergences confirmed by the #388 harness The #388 cross-backend screenshot-diff harness now renders the snippet corpus on web (SDL2/WebGL) and diffs against the desktop-headless oracle. On a calibration run over snippets 1–15 (`make test-wasm-diff SNIPPETS="1 6 8"`), clean static frames match to ≤2.2% pixel diff, and two snippets flagged real divergences: **1. Frame outline geometry (`008_frame_outline`, 8.5% diff) — SDL2-specific bug.** The divergence mask (`snippet-shots/web-diff/008_frame_outline_mask.png`) shows the frame *borders* differ, and the difference **scales with outline thickness** (frames at thickness 0/2/5/10/20 — the thickness-20 frame is grossly wrong, ~solid mismatch). Root cause is in `Shape::draw` (`SDL2Renderer.cpp`): the outline is built as per-edge quads extruded a full `outlineThickness` **outward** along each edge normal, with no corner miter/join and a different extrusion basis than `sf::Shape::setOutlineThickness`. Thin outlines are close; thick ones diverge badly. This is a concrete SDL2 outline-geometry defect → belongs in #390. **2. Text rendering (labels in the same frame differ) — the §3d divergence, confirmed.** The outline snippet's caption labels also show as differing pixels, corroborating the no-kerning / byte-wise-glyph text divergence catalogued in §3d. Already routed to #390. The harness passes clean frames and isolates genuine divergences with per-pixel masks for triage — exactly the CI signal this investigation called for. Full-corpus runs will surface the rest; each new divergence gets a mask and a line here / on #390.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
john/McRogueFace#347
No description provided.