Skip to content

Generate world and region imagery through the shared renderer #13

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

Keep this as the imagery delivery epic. Use deterministic complete scene bundles and the exact shared wgpu render path for client, editor, and atrinik-render; children own scene contexts, projections/products, CI packaging, and release evidence.

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

Summary

Add a deterministic offline world-image generator that executes the same compiled map renderer, sprite/effect code, lighting code, and asset lookup used by the legacy client. It should produce an unstyled, accurate isometric source render plus optional derived products suitable for minimaps, region maps, documentation, editor previews, and other tooling.

This is narrower than the native editor proposed in atrinik/editor#13: it establishes the reusable scene-capture, offscreen-render, projection-metadata, and export pipeline that the editor can later consume.

What exists today

The current atrinik-server --worldmaker pipeline is useful for navigation, but it is intentionally not a replica of the game view:

  • server/src/modules/world_maker.c defines each world tile as a 3x3 pixel box.
  • It decodes each face with libgd and reduces the image to an average RGB color.
  • render_object() filters the map to selected structural object types rather than drawing every client-visible layer.
  • It traverses only the four cardinal tiled-map links for image placement.
  • It records compatible TILED_UP maps in the .def metadata, but the PNG pass renders only each region's base horizontal map.
  • It has no client isometric projection, sprite alpha/compositing, multipart handling, animation, zoom/rotation/glow, tile stretching, unified cross-level painter order, smooth structural lighting, fog/cutaway behavior, or client effect path.

The exact rendering machinery already exists in the client:

  • client/src/gui/widgets/map.c:map_draw_map() draws every active physical depth through one global back-to-front painter queue.
  • client/src/client/sprite.c, tilestretcher.c, and animations.c own sprite transforms and animation behavior.
  • client/src/client/lighting.c owns ground and structural lighting.
  • The dynamic minimap already calls map_draw_map() on a separate surface, proving that the world path can render to a target other than the main map widget.

The current region-map consumer cannot simply swap in an isometric PNG. client/src/client/region_map.c and the region-map popup assume a rectangular image where pixel_size converts (map path, x, y) directly to image X/Y. Fog, player markers, panning, hit testing, labels, and tooltips all depend on that contract.

Proposed decision

Build a client-owned atrinik-map-render executable and a server-owned deterministic scene exporter, orchestrated by an updated generate-region-maps.sh (or a more general generate-world-images.sh).

Do not add another renderer to the server, Python tools, or a browser. The connected game and the offline generator must link and call the same renderer modules. A visual rule fixed in the client must therefore affect the next generated image without being reimplemented elsewhere.

The pipeline should be two-stage so the server runtime model and client renderer remain cleanly separated:

authored maps + collected archetypes/faces/animations
                    |
                    v
     server deterministic scene exporter
       (map loading and client-view semantics)
                    |
          versioned scene snapshots
                    |
                    v
     client-owned offscreen map renderer
        (the actual client render path)
                    |
        raw RGBA + projection metadata
                    |
                    v
     optional trusted export profiles
    (masks, grading, outlines, downsampling,
          labels, tiles/pyramids, packaging)

Architecture

1. Transport-independent server scene capture

Factor the scene-building portion of server/src/socket/request.c:draw_client_map2() away from socket delivery and incremental per-connection bookkeeping. It should be possible to produce a complete bounded client scene snapshot for a map/camera coordinate with an explicit visibility policy.

The first implementation may serialize length-delimited CLIENT_CMD_MAP payloads and replay them through the normal client command decoder. That is a strong equivalence fixture because the generator then populates the same MapCell representation as a connected client. The durable format should be versioned and snapshot-oriented rather than accidentally treating network delta state as a permanent file format.

The exporter must use normal server map loading and collected data, including archetype inheritance, multipart objects, map-space layer selection, linked physical levels, support height, light samples, and the same face/animation IDs exposed to the client. It must not invent another authored-map parser.

Keep player-dependent choices explicit. At minimum support:

  • world: omniscient static-world output, no fog, no player avatar/UI, no camera cutaway, no transient living/projectile/damage objects, and all intended structural depths visible;
  • player-view: the real LOS, fog, cutaway, special-vision, sub-layer, and depth semantics for a supplied map/coordinate, used for equivalence tests and diagnostic screenshots; and
  • named future profiles that select layers/entities without changing the renderer itself.

The snapshot manifest must record the policy, map/content digests, render clock, lighting/time-of-day inputs, resource catalog identity, and renderer settings. “Accurate” must always be reproducible rather than depending on ambient server time or mutable player state.

2. Reusable client renderer context and headless executable

Extract the map renderer's process globals/widget assumptions behind a context that accepts:

  • one or more physical-depth scene caches;
  • viewport/camera origin and output dimensions;
  • a renderer-owned resource provider;
  • a fixed render clock and animation policy;
  • lighting, fog, cutaway, annotation, and UI options; and
  • an explicit output target.

Both atrinik and atrinik-map-render must call this API. Do not copy map.c into a tool or maintain separate interactive/headless drawing branches.

The offline asset provider should load the collected bmaps, animations, and atrinik.0 catalog directly from isolated generation inputs. It must not connect to a server, depend on an existing user's cache/configuration, or silently omit missing faces. A missing or mismatched asset should fail generation with the face/map context.

For the current software client, the target can be an SDL memory surface. After atrinik/atrinik#128 and #17, the same executable should use an offscreen GPU render target plus readback; renderer ownership and scene APIs introduced there should be designed so an offline target is a normal consumer. Headless mode must fail clearly when no supported render backend is available instead of falling back to a divergent renderer.

3. Deterministic world composition

A complete region is larger than one client viewport. Rendering independent screenshots and placing them edge-to-edge will create seams because tall sprites, smooth lighting, floor masks, roofs, and stacked levels cross chunk boundaries.

Use one world projection and chunk it with guard bands:

  • derive horizontal map placement from the server's tiled-map graph and include linked physical depths;
  • project map/tile anchors through the same isometric transform used by the client;
  • compute conservative projected bounds from actual render commands/resources, including doubled, rotated, zoomed, stretched, multipart, tall, and effect sprites;
  • render each chunk with the required neighboring scene and light samples;
  • crop only the guard band after rendering; and
  • verify that the stitched chunks match an equivalent monolithic render on representative fixtures.

Do not composite complete physical levels or complete maps independently. Commands from adjacent maps and depths must enter the same painter order wherever their projected bounds overlap.

Freeze the render clock by default. Animated faces and glow/effect parameters should select a documented frame/time. Optional frame sequences or animated exports can be added later, but a normal build must produce byte-stable output for the same renderer/backend and inputs.

4. Replace scalar region-map coordinates with projection metadata

Emit a versioned manifest alongside the raw image or image tiles. It should contain:

  • image/tile-pyramid dimensions and content hashes;
  • the world-to-image projection basis and origin;
  • every map path/depth and its world placement;
  • a mapping from (map path, x, y, depth) to an image-space anchor or diamond/polygon;
  • labels, tooltips, hidden regions, and region membership currently emitted in .def files;
  • transparent/void coverage; and
  • the exact snapshot/render/export profile identities.

Update region-map fog, player markers, click-to-coordinate inspection, labels, and tooltip hit testing to use this transform. Fog should remain semantic map-tile state, then rasterize through the manifest's projected tile footprint; it should not become a fragile image-pixel history.

The greenfield posture allows replacing the current .def/single-PNG contract and updating all current consumers together. Do not retain both coordinate models solely for hypothetical old clients.

Large worlds should support chunked images and a zoom pyramid so the client does not decode one unbounded PNG. Small regions may still be packaged as one image when the same manifest contract is used.

5. Raw source render, auxiliary masks, and derived products

Always retain a lossless raw RGBA render before stylization. Export profiles may then derive purpose-specific assets without re-reading or reinterpreting map objects.

Where selective effects need semantic information, allow the same ordered render commands to produce auxiliary targets such as tile/world coordinates, structural-versus-transient coverage, depth, object class/layer, or emissive/light masks. These are alternate renderer materials/outputs, not a second geometry or painter implementation.

Example trusted profiles:

  • region: static structural scene, full region atlas/pyramid, labels and tooltips, fog/marker metadata;
  • minimap: static structural scene, reduced resolution, stronger silhouettes/contrast, optional color grading and outlines;
  • documentation: raw high-resolution render with transparent background and no navigation overlays; and
  • player-view: viewport-sized exact diagnostic output with the requested client settings.

Initially, deterministic CPU post-processing after the raw render is acceptable for simple resize, palette, contrast, or outline steps. Once #17/#131 land, reusable packaged material/postprocess stages should use that trusted renderer-owned effect system. Never accept server-supplied shader source, bytecode, arbitrary filesystem paths, or backend-specific resources.

6. Isolated generation and packaging

Preserve the useful behavior of the existing workflows while tightening isolation:

  • collect resources into a build/staging directory;
  • never create or modify source-tree server/data as a side effect;
  • build/run the native host exporter and renderer even when producing a MinGW server package;
  • cache output by authored content, collected catalogs, renderer code/version, profile, and relevant settings;
  • write to a temporary output directory and atomically replace only a complete successful generation; and
  • package the manifest, raw/derived images, and hashes through the server's existing authenticated asset-source path.

Phased delivery

Phase 0: equivalence proof

  • Capture a bounded complete map snapshot using the server's current client-map semantics.
  • Add a minimal offscreen client target that replays it through the normal client decoder and calls the existing map_draw_map().
  • Demonstrate pixel-identical output to /screenshot map under the software renderer with identical assets, clock, viewport, and settings.
  • Add fixtures for ordinary terrain, tall/multipart sprites, tile stretching, animation, smooth/discrete lighting, fog/cutaway, and depths +1/+2.

Phase 1: reusable contexts and deterministic profiles

Phase 2: world projection and chunk export

  • Traverse horizontal and vertical map topology into stable world coordinates.
  • Implement guard-band chunk rendering and cropping without cross-map/depth seams.
  • Emit raw RGBA plus the versioned projection/region metadata manifest.
  • Add monolithic-versus-chunked comparison tests and deterministic repeated-run hashes.

Phase 3: client consumption and products

  • Move region-map fog, markers, labels, tooltips, pan/zoom, and hit testing to projection metadata.
  • Add raw, region, minimap, and diagnostic profiles plus optional auxiliary masks.
  • Add chunk/zoom-pyramid streaming through the existing authenticated asset-source layer.
  • Replace the libgd averaged-color PNG path and remove its dependency when no consumer remains.

Phase 4: workflow integration

Acceptance criteria

  • The connected client and offline generator compile and call the same map rendering, sprite transform, lighting, animation, and painter-order modules; repository search finds no second map renderer in the new pipeline.
  • A captured player-view fixture matches the interactive client screenshot exactly under the current software backend. After GPU migration, comparison uses a documented small per-pixel tolerance across supported backends while painter/coverage masks remain exact.
  • Raw output correctly covers ordinary floors/masks/walls, tall and multipart objects, alpha, zoom/rotation/doubling, tile stretching/elevation, animation at a fixed clock, glow, smooth structural lighting, fog/cutaways, and at least depths +1 and +2.
  • Adjacent map and chunk boundaries have no missing/duplicated sprites, lighting discontinuities, clipping, or painter-order seams; chunked output matches a monolithic fixture.
  • The same inputs, profile, backend, and renderer version produce deterministic manifests and image hashes on repeated runs.
  • Region-map fog, player marker placement, coordinate inspection, labels, tooltips, and zoom/pan remain correct on the isometric output.
  • Missing assets, malformed snapshots, unsupported render backends, inconsistent topology, and partial output fail with actionable diagnostics and do not publish a mixed generation.
  • Generation uses isolated build/runtime directories, does not require a live network service or user cache, and never mutates authored maps, collected source aggregates, or source-tree runtime state.
  • Linux generation and native-host generation for Windows packaging pass with warnings as errors; representative visual fixtures run in CI on a supported deterministic backend.

Non-goals

  • Reimplementing the client renderer in libgd, Python, PHP, Canvas, or the next/ client.
  • Capturing mutable production server state or player-private information into packaged maps.
  • Making one ambiguous image that mixes static world truth with unspecified LOS, roof cutaway, animation, weather, or time-of-day state.
  • Replacing the authored map format or the server's authoritative runtime loader.
  • Requiring the full native editor from Replace Gridarta with the standalone MIT Rust editor editor#13 before command-line image generation is useful.

Relationships

The Phase 0 packet-replay proof can start on the current software renderer; the durable extraction and GPU implementation should be coordinated with those issues rather than building a temporary parallel renderer.

Child issues

These GitHub sub-issues are the executable delivery units tracked by atrinik/atrinik#168. Each belongs to one roadmap milestone; this parent remains open until all are complete.

M1

M2

  • atrinik/client#11 — World imagery: extract deterministic scene capture and headless render contexts

M3

  • atrinik/atrinik#202 — World imagery: generate deterministic projection and guard-band chunks

M4

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

    Fields

    No fields configured for Initiative.

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions