Shared Helpers
The windowed examples share a few modules. They are not registered as [[example]] targets;
each example pulls them in with mod common; and friends.
examples/common.rs
Run limits (GOLDY_EXAMPLE_TIMEOUT / EXAMPLE_TIMEOUT), the trailing FPS window used by the
GOLDY_PERF line, hidden-window creation so the first frame is never a blank flash,
and render_pipeline / render_pipeline_for_surface, which rebuild a pipeline against the
current colour-target format.
Present and readback stay in each example (SurfaceExchange::bind_render_target /
bind_destination and (&mut submission >> &present).take()?, or
(&mut submission >> &readback).take::<u8>() when
GOLDY_EXAMPLE_CAPTURE is set). CaptureDump is only the packed-RGBA file that
scripts/record_example_captures.sh stitches with ffmpeg. Optional:
GOLDY_EXAMPLE_CAPTURE_FRAMES (default 75), GOLDY_EXAMPLE_CAPTURE_FPS (default 15),
GOLDY_EXAMPLE_CAPTURE_WIDTH / HEIGHT (default 640×480).
//! Shared helpers for interactive examples (run limits, perf reporting, book captures).
//!
//! Set `GOLDY_EXAMPLE_CAPTURE` to a raw-RGBA output path to render headlessly
//! into pixels that `scripts/record_example_captures.sh` stitches with ffmpeg.
//! Optional: `GOLDY_EXAMPLE_CAPTURE_FRAMES` (default 75), `GOLDY_EXAMPLE_CAPTURE_FPS`
//! (default 15), `GOLDY_EXAMPLE_CAPTURE_WIDTH` / `HEIGHT` (default 640×480).
//!
//! Capture is file I/O plus a retained RGBA texture. Present still goes through
//! [`SurfaceExchange`] / [`MemoryExchange`] in each example.
use goldy::{
RenderPipeline, RenderPipelineDesc, Runtime, ShaderModule, SurfaceExchange, Texture, TextureFlags, TextureFormat,
TextureKind,
};
use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::PathBuf;
use std::time::{Duration, Instant};
use winit::event_loop::ActiveEventLoop;
use winit::window::{Window, WindowAttributes};
/// Window attributes for examples that reveal only after the first frame is ready.
#[allow(dead_code)]
pub fn hidden_window(title: impl Into<String>, width: u32, height: u32) -> WindowAttributes {
Window::default_attributes()
.with_title(title.into())
.with_inner_size(winit::dpi::LogicalSize::new(width, height))
.with_visible(false)
}
/// Show a window after GPU init and an initial present path have completed.
pub fn reveal_window(window: &Window) {
window.set_visible(true);
}
/// Rolling frame timestamps for windowed FPS (e.g. last 5s at exit).
#[allow(dead_code)]
pub struct FpsWindow {
window: Duration,
frames: Vec<Instant>,
}
#[allow(dead_code)]
impl FpsWindow {
pub fn new(window_secs: f64) -> Self {
Self {
window: Duration::from_secs_f64(window_secs),
frames: Vec::new(),
}
}
pub fn record(&mut self, now: Instant) {
self.prune(now);
self.frames.push(now);
}
fn prune(&mut self, now: Instant) {
let cutoff = now.checked_sub(self.window).unwrap_or(now);
let keep_from = self.frames.partition_point(|t| *t < cutoff);
if keep_from > 0 {
self.frames.drain(..keep_from);
}
}
/// Returns `(frames_in_window, window_span_secs, fps)` for the trailing window.
pub fn stats(&mut self, now: Instant) -> Option<(u64, f64, f64)> {
self.prune(now);
let n = self.frames.len();
if n == 0 {
return None;
}
let span = now.duration_since(self.frames[0]).as_secs_f64();
if span <= 0.0 {
return None;
}
Some((n as u64, span, n as f64 / span))
}
}
/// Build or rebuild a render pipeline for a colour-target format.
#[allow(dead_code)]
pub fn render_pipeline(
device: &Runtime,
shader: &ShaderModule,
format: TextureFormat,
desc: RenderPipelineDesc,
) -> anyhow::Result<RenderPipeline> {
RenderPipeline::new(
device,
shader,
shader,
&RenderPipelineDesc {
target_format: format,
..desc
},
)
}
/// Build or rebuild a render pipeline using the surface's current format.
#[allow(dead_code)]
pub fn render_pipeline_for_surface(
device: &Runtime,
shader: &ShaderModule,
surface: &SurfaceExchange,
desc: RenderPipelineDesc,
) -> anyhow::Result<RenderPipeline> {
render_pipeline(device, shader, surface.format(), desc)
}
/// True when this process should dump frames instead of opening a window.
pub fn capture_requested() -> bool {
match std::env::var("GOLDY_EXAMPLE_CAPTURE") {
Ok(path) => !path.is_empty(),
Err(_) => false,
}
}
fn env_u32(key: &str, default: u32) -> u32 {
std::env::var(key)
.ok()
.and_then(|raw| raw.parse().ok())
.filter(|n| *n > 0)
.unwrap_or(default)
}
fn env_f32(key: &str, default: f32) -> f32 {
std::env::var(key)
.ok()
.and_then(|raw| raw.parse().ok())
.filter(|n| *n > 0.0)
.unwrap_or(default)
}
/// Packed-RGBA dump for mdBook clips (`GOLDY_EXAMPLE_CAPTURE`). Not a Goldy type.
pub struct CaptureDump {
path: PathBuf,
writer: Option<BufWriter<File>>,
last: Option<Vec<u8>>,
fps: f32,
frames: u32,
written: u32,
width: u32,
height: u32,
}
#[allow(dead_code)]
impl CaptureDump {
fn capture_size() -> (u32, u32, u32, f32) {
(
env_u32("GOLDY_EXAMPLE_CAPTURE_WIDTH", 640),
env_u32("GOLDY_EXAMPLE_CAPTURE_HEIGHT", 480),
env_u32("GOLDY_EXAMPLE_CAPTURE_FRAMES", 75),
env_f32("GOLDY_EXAMPLE_CAPTURE_FPS", 15.0),
)
}
/// File dump from `GOLDY_EXAMPLE_CAPTURE`.
pub fn from_env() -> anyhow::Result<Self> {
let path = std::env::var("GOLDY_EXAMPLE_CAPTURE").expect("GOLDY_EXAMPLE_CAPTURE");
let path = PathBuf::from(path);
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() {
std::fs::create_dir_all(parent)?;
}
}
let (width, height, frames, fps) = Self::capture_size();
let file = File::create(&path)?;
Ok(Self {
path,
writer: Some(BufWriter::new(file)),
last: None,
fps,
frames,
written: 0,
width,
height,
})
}
/// In-memory frames only (e.g. `multi_window` panels before hstack).
pub fn memory(width: u32, height: u32) -> Self {
let fps = env_f32("GOLDY_EXAMPLE_CAPTURE_FPS", 15.0);
Self {
path: PathBuf::new(),
writer: None,
last: None,
fps,
frames: u32::MAX,
written: 0,
width,
height,
}
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn size(&self) -> (u32, u32) {
(self.width, self.height)
}
pub fn format() -> TextureFormat {
TextureFormat::Rgba8Unorm
}
/// Virtual clock `written / fps` so clips are deterministic.
pub fn time(&self) -> f32 {
self.written as f32 / self.fps
}
pub fn dt(&self) -> f32 {
1.0 / self.fps
}
pub fn finished(&self) -> bool {
self.written >= self.frames
}
pub fn write_rgba(&mut self, pixels: &[u8]) -> anyhow::Result<()> {
let expected = (self.width as usize)
.saturating_mul(self.height as usize)
.saturating_mul(4);
anyhow::ensure!(
pixels.len() == expected,
"capture frame is {} bytes, expected {expected} ({}x{} rgba)",
pixels.len(),
self.width,
self.height
);
if let Some(file) = &mut self.writer {
file.write_all(pixels)?;
}
self.last = Some(pixels.to_vec());
self.written += 1;
if let Some(file) = &mut self.writer {
if self.written >= self.frames {
file.flush()?;
println!(
"GOLDY_CAPTURE: wrote {} frames ({}x{} rgba) to {}",
self.written,
self.width,
self.height,
self.path.display()
);
}
}
Ok(())
}
pub fn take_rgba(&mut self) -> Option<Vec<u8>> {
self.last.take()
}
}
/// Retained RGBA8 texture for `copy_to_texture` plus a host claim on the capture path.
#[allow(dead_code)]
pub fn capture_readback(device: &Runtime, width: u32, height: u32) -> anyhow::Result<Texture> {
device.acquire_texture(
width,
height,
TextureFormat::Rgba8Unorm,
TextureKind::Direct,
TextureFlags::COPY_SRC | TextureFlags::COPY_DST,
None,
)
}
/// Horizontal concat of equal-sized packed RGBA panels (for `multi_window` capture).
#[allow(dead_code)]
pub fn hstack_rgba(panels: &[&[u8]], width: u32, height: u32) -> anyhow::Result<Vec<u8>> {
let row_bytes = width as usize * 4;
let panel_bytes = row_bytes * height as usize;
for (i, panel) in panels.iter().enumerate() {
anyhow::ensure!(
panel.len() == panel_bytes,
"hstack panel {i} is {} bytes, expected {panel_bytes}",
panel.len()
);
}
let n = panels.len();
let mut out = vec![0u8; panel_bytes.saturating_mul(n)];
for y in 0..height as usize {
for (i, panel) in panels.iter().enumerate() {
let src = y * row_bytes;
let dst = y * row_bytes * n + i * row_bytes;
out[dst..dst + row_bytes].copy_from_slice(&panel[src..src + row_bytes]);
}
}
Ok(out)
}
/// Run limit in seconds from `GOLDY_EXAMPLE_TIMEOUT` or `EXAMPLE_TIMEOUT`.
pub fn run_limit_secs() -> Option<f64> {
for key in ["GOLDY_EXAMPLE_TIMEOUT", "EXAMPLE_TIMEOUT"] {
if let Ok(raw) = std::env::var(key) {
if let Ok(secs) = raw.parse::<f64>() {
if secs > 0.0 {
return Some(secs);
}
}
}
}
None
}
/// Exit the event loop once the run limit elapses so `Drop` can print `GOLDY_PERF`.
pub fn exit_if_timed_out(event_loop: &ActiveEventLoop, start: Instant) {
if let Some(limit) = run_limit_secs() {
if start.elapsed() >= Duration::from_secs_f64(limit) {
event_loop.exit();
}
}
}
examples/digital_clock_shared.rs
Seven-segment digit geometry for digital_clock.
//! Shared digital-clock rendering helpers for the `digital_clock` example.
use goldy::types::Color;
/// Vertex with 2D position and RGBA color.
#[goldy::gpu]
#[derive(Debug)]
pub struct ClockVertex {
pub position: [f32; 2],
pub color: [f32; 4],
}
impl ClockVertex {
pub const fn new(x: f32, y: f32, color: Color) -> Self {
Self {
position: [x, y],
color: [color.r, color.g, color.b, color.a],
}
}
}
/// Seven-segment display patterns.
/// Order: top, top-left, top-right, middle, bottom-left, bottom-right, bottom
pub const SEGMENT_PATTERNS: [[bool; 7]; 11] = [
[true, true, true, false, true, true, true], // 0
[false, false, true, false, false, true, false], // 1
[true, false, true, true, true, false, true], // 2
[true, false, true, true, false, true, true], // 3
[false, true, true, true, false, true, false], // 4
[true, true, false, true, false, true, true], // 5
[true, true, false, true, true, true, true], // 6
[true, false, true, false, false, true, false], // 7
[true, true, true, true, true, true, true], // 8
[true, true, true, true, false, true, true], // 9
[false, false, false, false, false, false, false], // 10 = blank (for colon position)
];
pub const COLORS: [Color; 8] = [
Color {
r: 0.2,
g: 1.0,
b: 0.3,
a: 1.0,
},
Color {
r: 1.0,
g: 0.3,
b: 0.2,
a: 1.0,
},
Color {
r: 1.0,
g: 0.6,
b: 0.0,
a: 1.0,
},
Color {
r: 1.0,
g: 1.0,
b: 0.2,
a: 1.0,
},
Color {
r: 0.2,
g: 1.0,
b: 1.0,
a: 1.0,
},
Color {
r: 0.4,
g: 0.6,
b: 1.0,
a: 1.0,
},
Color {
r: 0.8,
g: 0.3,
b: 1.0,
a: 1.0,
},
Color {
r: 1.0,
g: 0.4,
b: 0.8,
a: 1.0,
},
];
pub fn quad_vertices(x: f32, y: f32, w: f32, h: f32, color: Color) -> [ClockVertex; 6] {
[
ClockVertex::new(x, y, color),
ClockVertex::new(x + w, y, color),
ClockVertex::new(x + w, y + h, color),
ClockVertex::new(x, y, color),
ClockVertex::new(x + w, y + h, color),
ClockVertex::new(x, y + h, color),
]
}
pub fn pixel_to_ndc(px: f32, py: f32, width: f32, height: f32) -> (f32, f32) {
let x = (px / width) * 2.0 - 1.0;
let y = 1.0 - (py / height) * 2.0;
(x, y)
}
pub fn digit_vertices(
digit: u8,
cx: f32,
cy: f32,
scale: f32,
color: Color,
width: f32,
height: f32,
) -> Vec<ClockVertex> {
let mut vertices = Vec::new();
let seg_w = 60.0 * scale;
let seg_h = 12.0 * scale;
let dig_h = 120.0 * scale;
let gap = 4.0 * scale;
if digit == 10 {
let dot_size = seg_h * 1.5;
let dot_spacing = dig_h * 0.5;
let (x, y) = pixel_to_ndc(cx - dot_size / 2.0, cy - dot_spacing - dot_size / 2.0, width, height);
let (w, h) = (dot_size / width * 2.0, dot_size / height * 2.0);
vertices.extend_from_slice(&quad_vertices(x, y, w, -h, color));
let (x, y) = pixel_to_ndc(cx - dot_size / 2.0, cy + dot_spacing - dot_size / 2.0, width, height);
vertices.extend_from_slice(&quad_vertices(x, y, w, -h, color));
return vertices;
}
let pattern = SEGMENT_PATTERNS[digit as usize];
let mut add_segment = |px: f32, py: f32, pw: f32, ph: f32| {
let (x, y) = pixel_to_ndc(px, py, width, height);
let (w, h) = (pw / width * 2.0, ph / height * 2.0);
vertices.extend_from_slice(&quad_vertices(x, y, w, -h, color));
};
if pattern[0] {
add_segment(cx - seg_w / 2.0, cy - dig_h, seg_w, seg_h);
}
if pattern[1] {
add_segment(
cx - seg_w / 2.0 - seg_h,
cy - dig_h + seg_h + gap,
seg_h,
dig_h - seg_h - gap * 2.0,
);
}
if pattern[2] {
add_segment(
cx + seg_w / 2.0,
cy - dig_h + seg_h + gap,
seg_h,
dig_h - seg_h - gap * 2.0,
);
}
if pattern[3] {
add_segment(cx - seg_w / 2.0, cy - seg_h / 2.0, seg_w, seg_h);
}
if pattern[4] {
add_segment(cx - seg_w / 2.0 - seg_h, cy + gap, seg_h, dig_h - seg_h - gap * 2.0);
}
if pattern[5] {
add_segment(cx + seg_w / 2.0, cy + gap, seg_h, dig_h - seg_h - gap * 2.0);
}
if pattern[6] {
add_segment(cx - seg_w / 2.0, cy + dig_h - seg_h, seg_w, seg_h);
}
vertices
}
#[derive(Debug, Clone, Copy, Default)]
pub struct TimeData {
pub hours: u8,
pub minutes: u8,
pub seconds: u8,
}
impl TimeData {
pub fn from_elapsed_secs(elapsed: u64) -> Self {
Self {
hours: ((elapsed / 3600) % 100) as u8,
minutes: ((elapsed % 3600) / 60) as u8,
seconds: (elapsed % 60) as u8,
}
}
pub fn to_digits(&self) -> [u8; 8] {
[
self.hours / 10,
self.hours % 10,
10,
self.minutes / 10,
self.minutes % 10,
10,
self.seconds / 10,
self.seconds % 10,
]
}
}
pub fn generate_clock_vertices(time: TimeData, color: Color, width: u32, height: u32) -> Vec<ClockVertex> {
let digits = time.to_digits();
let scale = height as f32 / 720.0;
let digit_width = 80.0 * scale;
let colon_width = 40.0 * scale;
let spacing = 20.0 * scale;
let total_width = digit_width * 6.0 + colon_width * 2.0 + spacing * 7.0;
let cy = height as f32 / 2.0;
let mut cx = (width as f32 - total_width) / 2.0 + digit_width / 2.0;
let mut all_vertices = Vec::new();
for &digit in digits.iter() {
let w = if digit == 10 { colon_width } else { digit_width };
let verts = digit_vertices(digit, cx, cy, scale, color, width as f32, height as f32);
all_vertices.extend_from_slice(&verts);
cx += w + spacing;
}
all_vertices
}
#[allow(dead_code)]
#[derive(Debug, Clone, Default)]
pub struct ClockState {
pub color_index: usize,
pub paused: bool,
pub accumulated_secs: u64,
}
#[allow(dead_code)]
impl ClockState {
pub fn color(&self) -> Color {
let mut color = COLORS[self.color_index];
if self.paused {
color.r *= 0.5;
color.g *= 0.5;
color.b *= 0.5;
}
color
}
pub fn background_color(&self) -> Color {
let bg = if self.paused { 0.06 } else { 0.02 };
Color {
r: bg,
g: bg,
b: bg,
a: 1.0,
}
}
pub fn next_color(&mut self) {
self.color_index = (self.color_index + 1) % COLORS.len();
}
pub fn toggle_pause(&mut self, current_elapsed: u64) {
if self.paused {
self.paused = false;
} else {
self.accumulated_secs = current_elapsed;
self.paused = true;
}
}
}
examples/instance2d.rs
The per-instance struct for instancing, laid out to match QuadInstance
in instancing_update.slang and instancing_render.slang.
//! Per-instance data for the instancing example.
#[goldy::gpu]
#[derive(Debug, Default)]
pub struct Instance2D {
pub position: [f32; 2],
pub rotation: f32,
pub scale: f32,
pub color: [f32; 4],
}
impl Instance2D {
pub const fn new(x: f32, y: f32, rotation: f32, scale: f32, color: [f32; 4]) -> Self {
Self {
position: [x, y],
rotation,
scale,
color,
}
}
}