Skip to content

Build the shared SDL3 and wgpu GPU renderer #17

Description

@zoeyrose

Important

This preserved product issue is now part of the fresh MIT replacement program. Final implementation owner: atrinik/renderer. Legacy C/SDL2, packet, global-state, and file-path details below are historical evidence only.

Replacement implementation contract

Implement one renderer/device owner for client window, editor viewport, and deterministic offscreen targets. Use SDL3 for platform/surface integration and wgpu/WGSL for rendering; do not perform a one-for-one software-surface port or maintain a second production backend.

New implementation and tests are independent MIT work unless an exact contribution by an approved MIT provenance grantor is admitted through the recorded file-level MIT grant. Preserve every player-facing, accessibility, disclosure, and performance design decision below.

Required verification

  • Test pure state/geometry/material/UI behavior headlessly where possible and run supported Linux/Windows integration paths.
  • Add bounded malformed, stale, lifecycle, resource-loss, and recovery cases appropriate to the owner.
  • Add shared Go/Rust protocol fixtures for authoritative fields; presentation never reconstructs hidden rules.
  • Use released shared renderer/protocol/toolkit contracts and wrapper-managed replacement scenarios.
Preserved product/design specification and historical implementation notes

Why

After atrinik/atrinik#128, Atrinik would run natively on SDL3 but would still render almost everything on the CPU through SDL_Surface, SDL_BlitSurface, SDL_gfx primitives, and a final software-surface presentation. Even excluding the bundled SDL_gfx/rotozoom implementations, the current client has 83 direct blit/fill call sites across 27 files. Widgets cache CPU surfaces, textures are surface-only, text is rasterized and blitted on the CPU, and the map path performs CPU transforms and per-pixel lighting before presenting the whole frame.

A GPU-backed renderer should reduce repeated CPU pixel work and full-frame transfers, provide batched sprite composition, make scaling/fullscreen/high-DPI presentation cleaner, and create a path for efficient lightmaps and other effects. The migration is high-risk because the map renderer has strict cross-level painter ordering, color-key transparency, generated surfaces, smooth structural lighting, zoom/rotation, fog, minimap, and screenshot behavior that must remain visually correct.

Target SDL 3.4 or newer and create the 2D renderer through SDL_CreateGPURenderer or the equivalent SDL GPU renderer properties. This retains the convenient SDL_Renderer texture/geometry/target API while enabling custom fragment shaders and resources through SDL_GPURenderState. Use the lower-level SDL GPU command-buffer/pipeline API only if measured requirements need custom vertex shaders, compute, indirect/instanced rendering, or render-pass behavior the 2D GPU renderer cannot express.

Depends on atrinik/atrinik#128. Follow-up optimization and effect work is tracked in atrinik/client#22 and #15.

Target state

  • One renderer abstraction owns the SDL GPU-backed 2D renderer, render targets, texture lifetime, frame begin/end, clipping, blend state, logical coordinates, presentation, and opaque material/effect state.
  • Static images and cached text live in SDL_Texture objects; mutable/generated content uploads only when its revision changes.
  • World, widgets, popups, overlays, and cursors emit GPU draw commands or render into GPU target textures instead of compositing through ScreenSurface.
  • Draw commands can carry a stable material identity and bounded parameters without exposing SDL_GPUShader, SDL_GPURenderState, or backend resources to gameplay, map, or UI code.
  • CPU surfaces remain only at deliberate boundaries such as image decoding, rare CPU-only generation/transforms while they are incrementally replaced, and screenshot/readback support. There is no maintained legacy full-frame software renderer.

Work

  • Capture representative visual screenshots and render-profiler baselines before changing the renderer: ordinary map, stacked depths +1/+2, smooth lighting, long walls/roofs, fog/cutaways, animated sprites, minimap, region map, text-heavy UI, popups, and alpha/color-key assets.
  • Require SDL 3.4 or newer and initialize the SDL GPU renderer backend explicitly. Report the selected renderer/GPU backend and fail clearly when no supported GPU path is available.
  • Introduce a small client-owned rendering API and make SDL_Renderer, window size/logical size, scale, clipping, blend modes, material state, and frame presentation private to it.
  • Define an opaque material field and parameter boundary on renderer draw commands. The default textured material is implemented here; the data-driven shader/material system is Build a data-driven wgpu shader and effect-material pipeline #15.
  • Validate the extension point with a minimal packaged SDL_GPURenderState fragment shader during development so the abstraction is not accidentally limited to fixed SDL draw state.
  • Redesign texture_struct to own GPU texture data and dimensions independently of SDL_Surface; define explicit creation, invalidation, reload, garbage collection, renderer/device reset, and teardown contracts.
  • Replace ScreenSurface, surface_show*, SDL_BlitSurface, SDL_FillRect, SDL_gfx line/box calls, and direct widget-to-screen blits with renderer commands in coherent vertical slices.
  • Cache TTF output as textures and batch UI composition. Preserve text markup, selection, cursor, outline, alignment, clipping, and UTF-8 behavior.
  • Preserve the unified back-to-front map painter order across physical depths. Ground/lightmap composition, floor masks, elevated sprites, camera cutaways, door halos, fog, and transparency must retain the invariants documented in client/AGENTS.md.
  • Represent smooth ground/structural lightmaps and other mutable raster data as revisioned streaming or target textures so uploads happen on invalidation, not every draw. Move hot transforms/effects to renderer geometry or texture modulation when visual equivalence permits.
  • Replace CPU rotozoom in hot presentation paths with renderer scaling/rotation; retain a narrowly scoped CPU transform only where generated pixel data truly requires it.
  • Move widget/map/minimap caches to target textures or retained draw data and ensure main-map and minimap caches do not invalidate one another.
  • Implement screenshots/readback, debug overlays, cursor behavior, resize/fullscreen/high-DPI coordinate conversion, renderer diagnostics, and graceful error handling.
  • Extend the render profiler with upload count/bytes, draw/material switches, renderer timing, and selected backend so regressions and upload churn are visible.
  • Remove obsolete software-presentation code and unused bundled SDL_gfx/rotozoom portions after the last consumer moves.
  • Update client architecture and visual-validation documentation with renderer ownership, resource lifetime, material boundaries, and the criteria for using raw SDL GPU APIs.

Acceptance criteria

  • Normal frame presentation uses the SDL GPU-backed SDL_Renderer and SDL_Texture; there is no window-sized ScreenSurface, SDL_Flip, direct window-surface blitting, or parallel legacy renderer.
  • Startup diagnostics identify the SDL renderer and underlying GPU backend, and supported Linux/Windows packages do not silently select the software renderer.
  • The renderer can create, bind, clear, and destroy a minimal custom SDL_GPURenderState without leaking GPU objects or exposing them outside the renderer module.
  • Static assets are uploaded once per load/reload and mutable resources upload only when dirty; profiler output exposes unexpected per-frame uploads.
  • Linux and portable MinGW client builds pass with warnings as errors and run through their supported SDL GPU renderer backends.
  • Before/after screenshots show equivalent painter ordering, lighting, transparency, sprite transforms, fog, map/minimap output, widgets, text, popups, and screenshots in all baseline scenes.
  • Mouse hit testing and text/cursor placement remain correct under resizing, fullscreen, logical scaling, and high-DPI output.
  • Before/after measurements are attached for at least two representative resolutions and both a map-heavy and UI-heavy scene, including frame CPU time, map-stage time, texture uploads, draw/material switches, and renderer/backend names. Any regression is explained and resolved or explicitly accepted.
  • Renderer/device creation, failure, teardown, and repeated resize/fullscreen cycles are exercised without leaks, stale textures, crashes, or corrupted output.

Design note

Do not start by translating every surface call one-for-one. Establish resource ownership, a draw-command boundary, and the opaque material extension point first, then migrate complete paths. That keeps GPU objects out of gameplay/UI code and avoids preserving the current surface coupling under new SDL names.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Fields

    Priority

    None yet

    Start date

    None yet

    Target date

    None yet

    Effort

    None yet

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions