Rendering Outputs

Surface manages a swapchain for zero-copy GPU-to-display presentation. It wraps the platform window handle, acquires drawable textures each frame, and presents finished frames to the display.

Creating a Surface

A Surface requires a Device and a window that implements HasWindowHandle + HasDisplayHandle (from the raw-window-handle crate).

#![allow(unused)]
fn main() {
use goldy::{Surface, SurfaceConfig, PresentMode, DepthFormat};

// Simplest form — Auto present mode, no depth buffer
let surface = Surface::new(&device, &window)?;

// With explicit configuration
let surface = Surface::new_with_config(&device, &window, SurfaceConfig {
    present_mode: PresentMode::Fifo,
    depth_format: Some(DepthFormat::Depth32Float),
})?;

// Shorthand for depth-only configuration
let surface = Surface::new_with_depth(&device, &window, Some(DepthFormat::Depth24Plus))?;
}

SurfaceConfig

#![allow(unused)]
fn main() {
pub struct SurfaceConfig {
    pub present_mode: PresentMode,
    pub depth_format: Option<DepthFormat>,
}
}
FieldPurposeDefault
present_modeVsync strategyAuto
depth_formatDepth buffer format, or None to disableNone

Present Modes

ModeBehaviorBackend Mapping
FifoVsync — wait for display refresh. No tearing, capped at monitor Hz.Metal displaySyncEnabled=YES, Vulkan FIFO, DX12 Present(1)
MailboxTriple-buffered — latest frame queued, older dropped. Low latency + no tearing.Vulkan MAILBOX. Falls back to Fifo on Metal and some DX12 configurations.
ImmediateNo sync, may tear. Maximum throughput for benchmarks.Metal displaySyncEnabled=NO, Vulkan IMMEDIATE, DX12 Present(0)
AutoGoldy chooses (Mailbox if available, then Fifo).

Change the present mode at runtime without recreating the surface:

#![allow(unused)]
fn main() {
surface.set_present_mode(PresentMode::Immediate)?;
let current = surface.present_mode();
}

Frame Acquisition Cycle

Each frame follows a begin → record → present sequence:

#![allow(unused)]
fn main() {
loop {
    // 1. Begin the frame (acquire a swapchain image)
    let frame = surface.begin()?;

    // 2. Record rendering commands
    let mut encoder = CommandEncoder::new();
    {
        let mut pass = encoder.begin_render_pass();
        pass.clear(Color::CORNFLOWER_BLUE);
        pass.set_pipeline(&pipeline);
        pass.set_vertex_buffer(0, &vertices);
        pass.draw(0..3, 0..1);
    }

    // 3. Submit and present
    frame.render(encoder)?;
    frame.present()?;
}
}

surface.acquire() is a legacy alias for surface.begin().

Frame

Frame represents a single swapchain image bracket. It tracks whether the frame has been presented and auto-presents on drop if you forget.

Frame Properties

#![allow(unused)]
fn main() {
let frame = surface.begin()?;

frame.width();   // frame dimensions (may differ from surface after resize)
frame.height();
}

Graphics Path — Frame::render

Record draw commands into a CommandEncoder and submit with render():

#![allow(unused)]
fn main() {
frame.render(encoder)?;
frame.present()?;
}

Compute Path — Frame::submit_compute

For compute-to-surface workflows, access the frame's texture directly and submit a TaskGraph:

#![allow(unused)]
fn main() {
let frame = surface.begin()?;
let tex = frame.texture();  // the swapchain texture as a storage image

// Build a task graph that writes to tex...
frame.submit_compute(&task_graph)?;
frame.present()?;
}

frame.texture() returns a &Texture with SpatialAccess::Direct, suitable for binding as a storage image in compute shaders.

Presenting

frame.present() consumes the Frame, submits all recorded work, and queues the image for display. It returns a TimelineValue that can be used with Device::wait_until().

#![allow(unused)]
fn main() {
let timeline = frame.present()?;
}

If a Frame is dropped without calling present(), it auto-presents to avoid leaking the swapchain image. This is safe but wastes a frame.

Surface Queries

#![allow(unused)]
fn main() {
surface.width();
surface.height();
surface.size();        // (width, height)
surface.format();      // TextureFormat of the swapchain images

// Validate that a pipeline's target format matches
surface.validate_pipeline_format(pipeline_format)?;
}

Resize Handling

Call resize() when the window size changes. Zero-size dimensions are silently ignored (common during window minimize).

#![allow(unused)]
fn main() {
fn on_resize(surface: &mut Surface, width: u32, height: u32) -> Result<()> {
    surface.resize(width, height)?;
    Ok(())
}
}

Texture Format

The swapchain format is chosen by the backend at surface creation (typically Bgra8UnormSrgb). Always use surface.format() when creating pipelines to ensure a match:

#![allow(unused)]
fn main() {
let desc = RenderPipelineDesc {
    target_format: surface.format(),
    ..Default::default()
};
}

Frame Lifetime

Frame follows Rust ownership semantics:

  • begin() acquires the swapchain image and returns a Frame
  • texture() borrows the frame — valid until present() is called
  • present() consumes the Frame — the borrow checker prevents use-after-present
  • Dropping without presenting auto-presents (prevents swapchain deadlock)
#![allow(unused)]
fn main() {
let frame = surface.begin()?;
let tex = frame.texture();
// tex is valid here
frame.present()?;
// tex is now invalid — Rust prevents accessing it
}