Rendering pipeline¶
This page documents the current live rendering path in the Python rewrite.
Scope:
- world/gameplay rendering in
src/crimson/render/world/* - the pre-draw terrain/FX bake step in
src/crimson/world/render_resources.py - the camera/viewport math used by both rendering and runtime camera updates
This is the live draw path. Headless simulation does not need RuntimeResources
or GPU textures and does not enter this pipeline.
Top-level frame flow¶
The same world renderer is used by gameplay modes, demo, replay playback, and the main debug views.
flowchart LR
A["Gameplay / Demo / Replay / Debug"] --> B["WorldRuntime.draw()"]
B --> C["RenderFrame + ViewTransform"]
C --> D["WorldRenderCtx"]
D --> E["draw_world()"]
Terrain FX are baked before drawing. RenderFrame carries concrete resources
and references to the current world; it does not copy simulation state.
Application compositing and resource lifetime¶
Alpha-test shaders are loaded lazily and owned by GroundRenderer and
RuntimeResources. Their owners release them before graphics teardown;
there is no process-wide cached shader handle. Terrain target/shader scopes
unwind on failure, and interrupted generation retains its pending seed.
The application loop applies non-default gamma to the completed frame, after world and UI shaders finish. Its intermediate framebuffer uses physical pixel dimensions while the captured scene retains logical drawing coordinates. The loop resizes and releases the framebuffer and gamma shader together.
Timed-bonus HUD slots carry one value for a global timer or a sequence in local player order. Speed, Shield, and Fire Bullets include all players. The 1P/2P bar positions remain native; 3P/4P extend the same vertical stack.
Runtime object graph¶
WorldRuntimeowns the camera and world size. Coordinate conversion derives a transform directly from those values and the current window dimensions.RenderResourcesowns the ground render target and pending terrain FX batches, borrows the application textures, and buildsRenderFrame.ViewTransformcontains the clamped camera, view scale, logical screen size, and output size. A draw computes it once and passes it through every pass.WorldRenderCtxcombines the frame and its transform. It has no back-reference to a mutable renderer or per-projectile projection overrides.WorldDrawContextcontains pass-specific textures, alpha, and overlay flags; projection data lives only inViewTransform.
Input coordinate conversion sees camera changes and window resizes immediately. An already prepared draw retains its captured transform.
Pre-draw terrain and FX bake¶
Before any world draw, callers run:
RenderResources.consume_terrain_fx_batch()andprocess_ground_pending()
That step consumes:
TerrainFxBatchfx_texturesGroundRenderer
and stamps decals, corpse imagery, and other terrain-bound FX into the ground render target.
flowchart LR
A["Simulation / presentation outputs"] --> B["TerrainFxBatch"]
B --> C["consume_terrain_fx_batch()"]
C --> D["bake_terrain_fx_batch(...)"]
D --> E["GroundRenderer render target"]
E --> F["draw_background()"]
This is why the terrain background pass can stay cheap during the main draw: most decal-like work has already been folded into the ground texture.
Render frame construction¶
RenderResources.build_render_frame() assembles the draw snapshot from:
- world geometry:
world_size,camera,ground - gameplay state:
state,players,creatures - resources: concrete
RuntimeResources - presentation toggles: elapsed time and bonus animation phase
- render mode:
rtx_mode
RenderFrame is the contract between the live runtime and the render tree.
Nothing below it should need to guess whether resources are available.
Main pass order¶
The main world pass lives in draw_world() in src/crimson/render/world/draw.py.
flowchart TD
A["draw_world(ctx with prepared transform)"] --> C["draw_background()"]
C --> D{"entity_alpha > 0?"}
D -- "no" --> Z["return"]
D -- "yes" --> E["build_draw_context()"]
E --> F["players_dead"]
F --> G["creatures"]
G --> H["freeze_overlay"]
H --> I["players_alive"]
I --> J["projectiles_effects"]
J --> K["bonus_ui"]
Background¶
draw_background():
- clears the backbuffer
- blits
GroundRendererusing the current camera/view window
The live world path now treats terrain as required. Missing ground is no
longer a supported fallback mode in draw_world().
Entity passes¶
The world entity passes run under _maybe_alpha_test(...), so terrain/entity
cutout behavior stays aligned with the classic fixed-function alpha-test path.
The shader shim is required; initialization failure is treated as a hard error.
The order is deliberate:
- dead players
- creatures
- freeze overlay
- living players
- projectiles and transient effects
- bonuses and UI-like overlays inside the world
Creature pass details¶
The creature pass is not a single flat loop.
flowchart TD
A["draw_creatures()"] --> B["Overlay pass over active pool"]
B --> C["Monster vision / plague / poison overlays"]
C --> D["Species sprite passes"]
D --> E["Zombie"]
D --> F["Spider SP1"]
D --> G["Spider SP2"]
D --> H["Alien"]
D --> I["Lizard"]
The sprite order mirrors the native pass structure:
- all active creature overlays first
- then fixed species buckets in native order
- pool order is preserved within each species bucket
That ordering matters for parity and should not be “simplified” into arbitrary sorting.
Projectile and effect branch¶
The projectile/effects branch is the busiest part of the frame.
flowchart TD
A["draw_projectiles_and_effects()"] --> B["laser_sight"]
B --> C["primary_projectiles"]
C --> D["particle_pool"]
D --> E["secondary_projectiles"]
E --> F["sprite_effect_pool"]
F --> G["effect_pool"]
Primary / secondary projectile rendering¶
Projectile draws use a projection-bound render context:
flowchart LR
A["WorldRenderCtx"] --> B["with_projection(camera, view_scale)"]
B --> C["ProjectileDrawCtx / SecondaryProjectileDrawCtx"]
C --> D["registry dispatch"]
D --> E["custom renderer if registered"]
D --> F["fallback atlas draw"]
Current behavior:
- primary and secondary projectile renderers try the registry path first
- if no specialized renderer handles the projectile, the code falls back to shared atlas-based drawing
- bullet trails and the Sharpshooter laser sight use low-level quad drawing
rather than only
draw_texture_pro(...)
Related modules:
src/crimson/render/world/projectiles.pysrc/crimson/render/projectile_draw/*src/crimson/render/projectile_render_registry.py
Bonus and world-UI pass¶
The final world-space pass is draw_bonus_and_ui().
It currently does:
- bonus pickup sprites
- hovered bonus labels
- aim indicators, if enabled and not in demo mode
- direction arrows
- aim enhancement sprites, if enabled and not in demo mode
This pass is still part of world rendering, not the out-of-world HUD.
Camera and viewport math¶
Viewport math now lives in src/crimson/render/world/viewport.py.
flowchart LR
A["world_size + config + camera + framebuffer size"] --> B["camera_screen_size()"]
B --> C["clamp_camera()"]
C --> D["view_transform()"]
D --> E["ViewTransform"]
E --> F["world_to_screen_with() / screen_to_world_with()"]
Three places use the same math:
WorldRuntime.update_camera()WorldRuntimeinput coordinate conversionsWorldRenderCtxdraw-time transforms
That keeps pre-draw camera updates and live rendering on one consistent set of transform rules.
Boundary rules¶
The current intended boundary is:
- headless sim and semantic presentation output stay resource-free
- live rendering starts only once concrete
RuntimeResourcesare available RenderFrameandWorldRenderCtxare live-draw types, not optional-resource compatibility shims- live world drawing also assumes
groundis initialized
Practical consequences:
- post-boot screens and gameplay rendering should assert resources once at the
boundary, not carry repeated
if resources is Nonebranches - missing terrain in the main world draw path is now treated as an invariant failure, not as a debug fallback
- terrain bootstrap is the only place that should still need registry lookups outside a bound live runtime
- render callsites should pass explicit
RenderFrameobjects instead of relying on implicit “active frame” state
Related docs¶
- Terrain (rewrite)
- Beam rendering (classic + RTX)
- Deterministic step pipeline
- Original exe rendering notes