1
Render context
Jaroslav Beneš edited this page 2026-08-08 18:11:58 +02:00

Render Context & GPU Setup

Stage 2 reference for oxide_engine::render — how the engine acquires the GPU and drives a window surface. For the event loop that calls into this each frame, see windowing.md.

Overview

Stage 2 rendering is deliberately minimal: acquire the GPU, configure the window surface, and clear it to a configurable color every frame. Meshes, materials, and passes arrive in Stage 4+. The module still establishes the two long-lived types every later stage builds on:

  • Gpu — instance, adapter, and the device/queue pair. Everything that touches the GPU goes through these four objects.
  • RenderContext — a Gpu plus a window's surface and its configuration; owns the per-frame acquire → clear → present cycle.

Both are created for you by run(); applications normally reach them through AppCtx::render().

Gpu

use oxide_engine::prelude::*;

let gpu = Gpu::headless()?;            // offscreen / tests
let device: &wgpu::Device = gpu.device();
let queue:  &wgpu::Queue  = gpu.queue();
# Ok::<(), oxide_engine::render::RenderError>(())

Acquisition asks for a high-performance adapter (compatible with the window surface in the windowed path) and a default-limits device. The chosen adapter and backend are logged at info level on startup.

Gpu::headless() skips the surface entirely — used by offscreen rendering and the automated Stage 2 integration test. Backend selection and debug flags remain overridable through wgpu's standard WGPU_* environment variables (e.g. WGPU_BACKEND=vulkan).

RenderContext

Owns the surface lifecycle:

  • Creation — builds the wgpu instance (the window doubles as the display handle), creates the surface, acquires the Gpu, and configures the surface with get_default_config (the platform's preferred format and present mode).
  • resize(width, height) — reconfigures the surface. Zero dimensions (minimized windows) are clamped to 1 so the surface stays valid. Called automatically by the event loop on Resized.
  • set_clear_color(color) / clear_color() — the color the next frame is cleared to. The engine's Color is linear f32 RGBA, matching what the surface expects (conversion to wgpu::Color is render::to_wgpu_color).
  • render_frame() — one frame: acquire the next surface texture, record a clear pass, submit, present.
  • size(), gpu() — current surface size (physical pixels) and the underlying Gpu.

Frame acquisition and transient failures

get_current_texture can fail for reasons that are normal during resizes and window-manager activity. render_frame() maps them as follows:

Surface state Behavior
Success / Suboptimal Clear and present (a suboptimal frame is still presentable; the next resize reconfigures anyway)
Lost / Outdated Reconfigure the surface, skip the frame
Timeout / Occluded Skip the frame
Validation Returned as RenderError::SurfaceValidation — a real bug, not transient

Skipped frames are invisible in practice: the next RedrawRequested arrives within milliseconds.

clear_view

The single render operation Stage 2 owns:

oxide_engine::render::clear_view(device, queue, &texture_view, Color::RED);

Records and submits a render pass whose only work is a load-op clear. Both the windowed path (render_frame) and offscreen targets go through it, which is what makes the GPU path automatically testable: the integration test stage2::headless_clear_fills_texture_with_clear_color clears an offscreen texture headless, reads the pixels back, and asserts the exact clear color — no window or human needed. (It self-skips on machines with no GPU adapter.)

Errors

RenderError (a thiserror enum) distinguishes the failure modes callers might handle: NoAdapter, Device, CreateSurface, UnsupportedSurface, and SurfaceValidation. Binaries typically just propagate it via anyhow out of run().

Design notes

  • The window is held as Arc<winit::window::Window> so the surface, which borrows the window, can be 'static — winit hands windows out from inside its event loop, and wgpu surfaces must outlive every frame.
  • Gpu and RenderContext are separate types on purpose: later stages (and tests today) need the device/queue without any window, and composability is a core project principle.