Skip to content

Frame Rendering Pipeline

The frame pipeline defines the exact order used to update runtime state, compose the world view, add overlays, and finally present the image to the window. In this codebase, that order is fixed and intentional.

Per-frame order

`draw_frame(app)` builds the whole image in one deterministic pass

The frame loop updates state first, renders world layers in order, then presents one final composed image. This ordering is what keeps gameplay, layering, and overlays coherent.

mlx_loop_hook(..., draw_frame, app)

`draw_frame` runs once per loop tick and owns the whole render pipeline.

  1. 1
    update timing state

    refresh `delta_time` and clamp large spikes

  2. 2
    update_player_input state

    consume intent and update player position / camera

  3. 3
    update bonus doors bonus

    door transitions and passability state

  4. 4
    update bonus HUD bonus

    timers, face state, and transient HUD state

  5. 5
    update bonus pickups bonus

    contact checks and stat changes before render

  6. 6
    retro_begin bonus

    swap frame target if retro mode is active

  7. 7
    draw background world

    clear ceiling and floor colors into the frame buffer

  8. 8
    raycast_scene world

    cast one ray per column and draw walls

  9. 9
    draw sprites bonus

    project and depth-test bonus sprites

  10. 10
    draw minimap bonus

    overlay local map information onto the world view

  11. 11
    draw vignette post

    apply post-world shading or edge darkening

  12. 12
    retro_render bonus

    upscale retro framebuffer into output image when enabled

  13. 13
    draw HUD bonus

    final screen-space overlay drawn after world effects

  14. 14
    mlx_put_image_to_window present

    single final blit of the composed frame

The render loop is driven by MLX:

  • mlx_loop_hook(..., draw_frame, app)

Each loop tick executes draw_frame(app) once. That function is the single entry point for all per-frame gameplay updates and rendering work.

The current runtime order is:

  1. update timing
  2. update player input
  3. update bonus doors
  4. update bonus HUD state
  5. update bonus pickups
  6. begin retro path if active
  7. draw ceiling and floor background
  8. raycast and draw walls
  9. draw bonus sprites
  10. draw minimap
  11. apply vignette / post-process
  12. upscale retro output if needed
  13. draw HUD last
  14. present the final image

This ordering keeps gameplay state coherent with the frame being drawn.

Frame buffer

A frame is built layer by layer inside `frame.addr`

Each loop tick writes the background first, then textured walls on top. The window receives the image only at the end with one mlx_put_image_to_window call.

ceiling written
if (y < app->frame.height / 2)
    put_pixel(&app->frame, x, y, app->ceiling_color);

What is read

  • `app->ceiling_color` and `app->floor_color` are drawable ints.
  • `app->tex_no/so/we/ea.addr` contains pixels loaded during `init_mlx`.
  • `app->map.grid` is read by raycasting to find walls.

What is written

  • `frame.addr` receives ceiling and floor first.
  • Wall columns overwrite only the region between `draw_start` and `draw_end`.
  • `frame.img_ptr` is sent to the window when the image is complete.

Layer priority matters:

  • world first
  • gameplay overlays after world
  • post-process after world layers
  • HUD last
  • one final present call

That prevents overlays from being overwritten and keeps the UI readable.

The mandatory path still goes through the same draw_frame(...) entry point. Bonus systems are integrated through dedicated modules and no-op fallbacks, so shared calls remain safe when a feature is inactive.

If retro mode is disabled, the normal frame buffer is presented directly. If it is enabled, the world is rendered to the retro target and then upscaled before final presentation.

Frame timing is recomputed every frame and used by movement and update logic.

The main stability properties are:

  • first-frame fallback timing
  • delta clamping after stalls
  • no expected allocation in the steady render path; rebuilds are tied to specific state changes

This avoids large jumps in player motion and keeps the pipeline predictable.

The pipeline ends with one mlx_put_image_to_window(...) call.

That matters because the engine writes pixels into memory first, then performs a single display flush, which is much cleaner and cheaper than per-pixel window calls.

  • srcs/core/main.c
  • srcs/render/render_frame.c
  • srcs/render/raycast.c
  • srcs/render/raycast_draw.c
  • srcs/render/draw_vignette.c
  • srcs_bonus/retro/api.c
  • srcs_bonus/sprites/
  • srcs_bonus/hud/