multi_window
Three windows — plasma, tunnel, and starfield — sharing one Runtime. Each window owns its
own SurfaceExchange, Context, and Scheme, which is the pattern for any multi-surface
application.
cargo run --features examples --example multi_window
What it demonstrates
- Multiple surfaces on a single device
- One scheme and context per window
- Independent resize and close handling per window
Controls
| Key | Action |
|---|---|
Space | Toggle the focused window's effect modifier |
R | Reset the focused window's effect |
Escape | Close the focused window; the app exits with the last one |
Source
examples/multi_window.rs:
//! Multi-window example - three simultaneous effects in separate windows.
//!
//! Each window runs its own demo with an independent SurfaceExchange + Scheme.
//!
//! Run with: cargo run --example multi_window
use goldy::{
shaders, Buffer, BufferFlags, BufferKind, Color, DepositTarget, DepositTransaction, Instance, Lease,
LeaseRenderTarget, MemoryExchange, NodeAccess, RenderPipeline, RenderPipelineDesc, RequestAdapterOptions,
RuntimeDescriptor, Scheme, ShaderModule, SurfaceConfig, SurfaceExchange, TargetLoad, Texture, TextureFormat,
Transaction, VertexBufferLayout,
};
mod common;
use common::CaptureDump;
use std::ops::Shr;
const PLASMA_VERTEX_TIME: &str = r#"
struct VertexInput {
float2 position : POSITION;
float2 uv : TEXCOORD0;
float time : TEXCOORD1;
};
struct VertexOutput {
float4 position : SV_Position;
float2 uv : TEXCOORD0;
float time : TEXCOORD1;
};
[shader("vertex")]
VertexOutput vs_main(VertexInput input) {
VertexOutput output;
output.position = float4(input.position, 0.0, 1.0);
output.uv = input.uv;
output.time = input.time;
return output;
}
float3 rainbow(float t) {
float3 c = float3(
sin(t * 6.28318 + 0.0) * 0.5 + 0.5,
sin(t * 6.28318 + 2.094) * 0.5 + 0.5,
sin(t * 6.28318 + 4.189) * 0.5 + 0.5
);
return c;
}
[shader("fragment")]
float4 fs_main(VertexOutput input) : SV_Target {
float2 uv = input.uv * 4.0;
float t = input.time;
float v = sin(uv.x + t);
v += sin(uv.y + t);
v += sin(uv.x + uv.y + t);
float cx = uv.x + 0.5 * sin(t / 3.0);
float cy = uv.y + 0.5 * cos(t / 2.0);
v += sin(sqrt(cx * cx + cy * cy + 1.0) + t);
v = v / 2.0;
return float4(rainbow(v), 1.0);
}
"#;
const TUNNEL_VERTEX_TIME: &str = r#"
struct VertexInput {
float2 position : POSITION;
float2 uv : TEXCOORD0;
float time : TEXCOORD1;
};
struct VertexOutput {
float4 position : SV_Position;
float2 uv : TEXCOORD0;
float time : TEXCOORD1;
};
[shader("vertex")]
VertexOutput vs_main(VertexInput input) {
VertexOutput output;
output.position = float4(input.position, 0.0, 1.0);
output.uv = input.uv;
output.time = input.time;
return output;
}
[shader("fragment")]
float4 fs_main(VertexOutput input) : SV_Target {
float2 uv = (input.uv - 0.5) * 2.0;
float t = input.time;
float dist = length(uv);
float angle = atan2(uv.y, uv.x);
float tunnel_depth = 1.0 / (dist + 0.1);
float tunnel_angle = angle / 3.14159 + t * 0.2;
float tx = tunnel_angle * 4.0;
float ty = tunnel_depth - t * 2.0;
float checker = floor(tx) + floor(ty);
bool is_white = fmod(checker, 2.0) == 0.0;
float depth_color = 1.0 - dist * 0.5;
float3 color;
if (is_white) {
color = float3(0.8, 0.2, 0.4) * depth_color;
} else {
color = float3(0.2, 0.4, 0.8) * depth_color;
}
color += float3(0.3, 0.5, 1.0) * (1.0 - dist) * (1.0 - dist);
color *= 1.0 - dist * 0.3;
return float4(color, 1.0);
}
"#;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Instant;
use winit::{
application::ApplicationHandler,
dpi::LogicalSize,
event::{ElementState, MouseButton, WindowEvent},
event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
keyboard::{Key, NamedKey},
window::{Window, WindowAttributes, WindowId},
};
#[goldy::gpu]
struct QuadVertex {
position: [f32; 2],
uv: [f32; 2],
time: f32,
}
impl QuadVertex {
fn layout() -> VertexBufferLayout {
Self::GPU_TYPE.vertex_buffer_layout().expect("quad vertex layout")
}
}
fn create_quad(time: f32) -> [QuadVertex; 6] {
[
QuadVertex {
position: [-1.0, -1.0],
uv: [0.0, 1.0],
time,
},
QuadVertex {
position: [1.0, -1.0],
uv: [1.0, 1.0],
time,
},
QuadVertex {
position: [1.0, 1.0],
uv: [1.0, 0.0],
time,
},
QuadVertex {
position: [-1.0, -1.0],
uv: [0.0, 1.0],
time,
},
QuadVertex {
position: [1.0, 1.0],
uv: [1.0, 0.0],
time,
},
QuadVertex {
position: [-1.0, 1.0],
uv: [0.0, 0.0],
time,
},
]
}
#[derive(Clone, Copy, PartialEq)]
enum EffectType {
Plasma,
Tunnel,
Starfield,
}
impl EffectType {
fn title(&self) -> &'static str {
match self {
EffectType::Plasma => "Plasma [Space=pause, Click=reset]",
EffectType::Tunnel => "Tunnel [Space=reverse, Click=reset]",
EffectType::Starfield => "Starfield [Space=warp, Click=reset]",
}
}
fn shader_source(&self) -> &'static str {
match self {
EffectType::Plasma => PLASMA_VERTEX_TIME,
EffectType::Tunnel => TUNNEL_VERTEX_TIME,
EffectType::Starfield => shaders::STARFIELD,
}
}
}
struct WindowState {
window: Option<Arc<Window>>,
ctx: goldy::Context,
surface: Option<SurfaceExchange>,
present: Option<Transaction>,
capture: Option<CaptureDump>,
readback: Option<Texture>,
scheme: Scheme,
scene_rt: Lease<LeaseRenderTarget>,
pipeline: RenderPipeline,
effect_type: EffectType,
start_time: Instant,
paused: bool,
paused_at: f32,
time_multiplier: f32,
vertex_parcel: Buffer,
upload_scheme: Scheme,
vertex_deposit: DepositTransaction,
has_focus: bool,
}
impl WindowState {
fn create_pipeline(
device: &goldy::Runtime,
shader: &ShaderModule,
format: TextureFormat,
) -> anyhow::Result<RenderPipeline> {
common::render_pipeline(
device,
shader,
format,
RenderPipelineDesc {
vertex_layout: QuadVertex::layout(),
..Default::default()
},
)
}
fn record_pass(
scheme: &mut Scheme,
pipeline: &RenderPipeline,
vertex_parcel: &Buffer,
scene_rt: &Lease<LeaseRenderTarget>,
label: &'static str,
) {
let mut pass = scheme.render_pass(label, scene_rt, TargetLoad::Clear(Color::BLACK));
pass.with_parcel(vertex_parcel, NodeAccess::Read);
pass.set_pipeline(pipeline);
pass.set_vertex_buffer(0, vertex_parcel);
pass.draw(0..6, 0..1);
pass.finish();
}
fn bind_frame(
scheme: &mut Scheme,
scene_rt: &Lease<LeaseRenderTarget>,
surface: Option<&SurfaceExchange>,
readback: Option<&Texture>,
) -> anyhow::Result<Option<Transaction>> {
if let Some(surface) = surface {
let present = surface.bind_render_target(scheme, scene_rt)?;
Ok(Some(present))
} else {
let readback = readback.expect("capture readback");
scheme.copy_to_texture(scene_rt, readback)?;
Ok(None)
}
}
fn output_size(&self) -> (u32, u32) {
if let Some(surface) = &self.surface {
surface.size()
} else {
self.capture.as_ref().expect("capture").size()
}
}
fn output_format(&self) -> TextureFormat {
if let Some(surface) = &self.surface {
surface.format()
} else {
CaptureDump::format()
}
}
fn rerecord_scheme(&mut self) {
let mut scheme = Scheme::new(&self.ctx);
let (width, height) = self.output_size();
let format = self.output_format();
if let Ok(rt) = self.ctx.lease_render_target(width.max(1), height.max(1), format, None) {
Self::record_pass(
&mut scheme,
&self.pipeline,
&self.vertex_parcel,
&rt,
self.effect_type.title(),
);
if let Ok(present) = Self::bind_frame(&mut scheme, &rt, self.surface.as_ref(), self.readback.as_ref()) {
self.present = present;
self.scene_rt = rt;
self.scheme = scheme;
}
}
}
fn windowed(
window: Arc<Window>,
ctx: &goldy::Context,
device: &Arc<goldy::Runtime>,
effect_type: EffectType,
) -> anyhow::Result<Self> {
let surface = SurfaceExchange::new(ctx, window.as_ref(), SurfaceConfig::default())?;
let format = surface.format();
let (width, height) = surface.size();
let shader = ShaderModule::from_slang(device, effect_type.shader_source())?;
let pipeline = Self::create_pipeline(device, &shader, format)?;
let vertex_parcel =
device.acquire_buffer_sized::<QuadVertex>(6, BufferKind::Scattered, BufferFlags::empty())?;
let mut scheme = Scheme::new(ctx);
let scene_rt = ctx.lease_render_target(width.max(1), height.max(1), format, None)?;
Self::record_pass(&mut scheme, &pipeline, &vertex_parcel, &scene_rt, effect_type.title());
let present = Self::bind_frame(&mut scheme, &scene_rt, Some(&surface), None)?;
let mut upload_scheme = Scheme::new(ctx);
let vertex_deposit = MemoryExchange::new(ctx).bind_deposit(
&mut upload_scheme,
DepositTarget::buffer(&vertex_parcel, vertex_parcel.byte_size()),
)?;
Ok(Self {
window: Some(window),
ctx: ctx.clone(),
surface: Some(surface),
present,
capture: None,
readback: None,
scheme,
scene_rt,
pipeline,
effect_type,
start_time: Instant::now(),
paused: false,
paused_at: 0.0,
time_multiplier: 1.0,
vertex_parcel,
upload_scheme,
vertex_deposit,
has_focus: false,
})
}
fn capture_panel(
ctx: &goldy::Context,
device: &Arc<goldy::Runtime>,
effect_type: EffectType,
width: u32,
height: u32,
) -> anyhow::Result<Self> {
let capture = CaptureDump::memory(width, height);
let format = CaptureDump::format();
let readback = common::capture_readback(&device, width, height)?;
let shader = ShaderModule::from_slang(device, effect_type.shader_source())?;
let pipeline = Self::create_pipeline(device, &shader, format)?;
let vertex_parcel =
device.acquire_buffer_sized::<QuadVertex>(6, BufferKind::Scattered, BufferFlags::empty())?;
let mut scheme = Scheme::new(ctx);
let scene_rt = ctx.lease_render_target(width.max(1), height.max(1), format, None)?;
Self::record_pass(&mut scheme, &pipeline, &vertex_parcel, &scene_rt, effect_type.title());
let present = Self::bind_frame(&mut scheme, &scene_rt, None, Some(&readback))?;
let mut upload_scheme = Scheme::new(ctx);
let vertex_deposit = MemoryExchange::new(ctx).bind_deposit(
&mut upload_scheme,
DepositTarget::buffer(&vertex_parcel, vertex_parcel.byte_size()),
)?;
Ok(Self {
window: None,
ctx: ctx.clone(),
surface: None,
present,
capture: Some(capture),
readback: Some(readback),
scheme,
scene_rt,
pipeline,
effect_type,
start_time: Instant::now(),
paused: false,
paused_at: 0.0,
time_multiplier: 1.0,
vertex_parcel,
upload_scheme,
vertex_deposit,
has_focus: false,
})
}
fn current_time(&self) -> f32 {
if let Some(capture) = &self.capture {
return capture.time();
}
if self.paused {
self.paused_at
} else {
self.paused_at + self.start_time.elapsed().as_secs_f32() * self.time_multiplier
}
}
fn toggle_pause(&mut self) {
if self.paused {
self.start_time = Instant::now();
self.paused = false;
} else {
self.paused_at = self.current_time();
self.paused = true;
}
}
fn toggle_effect_modifier(&mut self) {
match self.effect_type {
EffectType::Plasma => self.toggle_pause(),
EffectType::Tunnel => {
self.paused_at = self.current_time();
self.start_time = Instant::now();
self.time_multiplier *= -1.0;
}
EffectType::Starfield => {
self.paused_at = self.current_time();
self.start_time = Instant::now();
self.time_multiplier = if self.time_multiplier > 2.0 { 1.0 } else { 5.0 };
}
}
}
fn reset(&mut self) {
self.start_time = Instant::now();
self.paused = false;
self.paused_at = 0.0;
self.time_multiplier = 1.0;
}
fn render(&mut self, _ctx: &goldy::Context) -> anyhow::Result<()> {
if let Some(window) = &self.window {
let size = window.inner_size();
if size.width == 0 || size.height == 0 {
return Ok(());
}
}
let vertices = create_quad(self.current_time());
(&self.vertex_deposit << vertices.as_slice())?;
self.upload_scheme.submit()?;
let mut submission = self.scheme.submit()?;
if let Some(present) = &self.present {
(&mut submission >> present).take()?;
} else {
let pixels = (&mut submission >> self.readback.as_ref().unwrap())
.take::<u8>()?
.to_vec();
self.capture.as_mut().unwrap().write_rgba(&pixels)?;
}
Ok(())
}
fn handle_resize(&mut self, width: u32, height: u32) {
if width == 0 || height == 0 {
return;
}
let Some(surface) = &self.surface else {
return;
};
let (prev_w, prev_h) = surface.size();
// Pipeline does not depend on surface size; skip no-op Resized events
// (winit often fires these on reveal) so we don't recompile Slang→DXIL.
if prev_w == width && prev_h == height {
return;
}
let _ = surface.resize(width, height);
self.rerecord_scheme();
}
}
struct App {
instance: Instance,
ctx: Option<goldy::Context>,
device: Option<Arc<goldy::Runtime>>,
windows: HashMap<WindowId, WindowState>,
effects_to_create: Vec<EffectType>,
frame_count: u32,
start_time: std::time::Instant,
}
impl App {
fn new() -> anyhow::Result<Self> {
Ok(Self {
instance: Instance::new()?,
ctx: None,
device: None,
windows: HashMap::new(),
effects_to_create: vec![EffectType::Plasma, EffectType::Tunnel, EffectType::Starfield],
frame_count: 0,
start_time: std::time::Instant::now(),
})
}
fn create_window(
&mut self,
event_loop: &ActiveEventLoop,
effect_type: EffectType,
position: (i32, i32),
) -> anyhow::Result<()> {
let device = self.device.as_ref().unwrap().clone();
let ctx = self.ctx.as_ref().unwrap();
let attrs = WindowAttributes::default()
.with_title(format!("Goldy - {}", effect_type.title()))
.with_inner_size(LogicalSize::new(500, 500))
.with_position(winit::dpi::LogicalPosition::new(position.0, position.1))
.with_visible(false);
let window = Arc::new(event_loop.create_window(attrs)?);
let window_id = window.id();
let mut state = WindowState::windowed(window.clone(), ctx, &device, effect_type)?;
state.render(ctx)?;
common::reveal_window(&window);
window.request_redraw();
self.windows.insert(window_id, state);
Ok(())
}
}
impl ApplicationHandler for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.device.is_none() {
match self
.instance
.request_adapter(&RequestAdapterOptions::default())
.and_then(|a| a.request_runtime(&RuntimeDescriptor::default()))
{
Ok(device) => {
let device = Arc::new(device);
self.ctx = Some(device.create_context().expect("create context"));
self.device = Some(device);
}
Err(e) => {
tracing::error!("Failed to create device: {}", e);
event_loop.exit();
return;
}
}
}
let effects = std::mem::take(&mut self.effects_to_create);
for (i, effect) in effects.into_iter().enumerate() {
let x = 50 + (i as i32) * 520;
let y = 100;
if let Err(e) = self.create_window(event_loop, effect, (x, y)) {
tracing::error!("Failed to create window for {:?}: {}", effect.title(), e);
}
}
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, window_id: WindowId, event: WindowEvent) {
let state = match self.windows.get_mut(&window_id) {
Some(s) => s,
None => return,
};
match event {
WindowEvent::CloseRequested => {
self.windows.remove(&window_id);
if self.windows.is_empty() {
event_loop.exit();
}
}
WindowEvent::Focused(focused) => {
state.has_focus = focused;
if focused {
println!(
"Focus: {} ({})",
state.effect_type.title(),
if state.paused { "paused" } else { "running" }
);
}
}
WindowEvent::KeyboardInput { event, .. } if event.state == ElementState::Pressed => {
match event.logical_key {
Key::Named(NamedKey::Escape) => {
self.windows.remove(&window_id);
if self.windows.is_empty() {
event_loop.exit();
}
}
Key::Named(NamedKey::Space) => {
if let Some(s) = self.windows.get_mut(&window_id) {
s.toggle_effect_modifier();
println!("[{}] Modifier toggled", s.effect_type.title());
}
}
Key::Character(ref c) if c == "r" || c == "R" => {
if let Some(s) = self.windows.get_mut(&window_id) {
s.reset();
println!("[{}] Reset", s.effect_type.title());
}
}
_ => {}
}
}
WindowEvent::MouseInput {
state: ElementState::Pressed,
button: MouseButton::Left,
..
} => {
if let Some(s) = self.windows.get_mut(&window_id) {
s.reset();
println!("[{}] Reset (click)", s.effect_type.title());
}
}
WindowEvent::RedrawRequested => {
let Some(ctx) = self.ctx.clone() else {
return;
};
if let Some(s) = self.windows.get_mut(&window_id) {
if let Err(e) = s.render(&ctx) {
tracing::error!("[{}] Render error: {}", s.effect_type.title(), e);
}
}
}
WindowEvent::Resized(new_size) => {
if let Some(s) = self.windows.get_mut(&window_id) {
s.handle_resize(new_size.width, new_size.height);
}
}
_ => {}
}
}
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
common::exit_if_timed_out(event_loop, self.start_time);
if self.ctx.is_none() {
return;
}
self.frame_count += 1;
for state in self.windows.values() {
if let Some(window) = &state.window {
window.request_redraw();
}
}
}
}
impl Drop for App {
fn drop(&mut self) {
let elapsed = self.start_time.elapsed().as_secs_f64();
let fps = if elapsed > 0.0 {
self.frame_count as f64 / elapsed
} else {
0.0
};
println!(
"GOLDY_PERF: frames={} elapsed={elapsed:.2}s avg_fps={fps:.1}",
self.frame_count
);
}
}
fn capture_panels() -> anyhow::Result<()> {
let instance = Instance::new()?;
let device = Arc::new(
instance
.request_adapter(&RequestAdapterOptions::default())?
.request_runtime(&RuntimeDescriptor::default())?,
);
let ctx = device.create_context()?;
let mut output = CaptureDump::from_env()?;
let (out_w, out_h) = output.size();
anyhow::ensure!(out_w % 3 == 0, "multi_window capture width must be divisible by 3");
let panel = (out_w / 3, out_h);
let effects = [EffectType::Plasma, EffectType::Tunnel, EffectType::Starfield];
let mut panels = Vec::new();
for effect in effects {
panels.push(WindowState::capture_panel(&ctx, &device, effect, panel.0, panel.1)?);
}
while !output.finished() {
let mut frames = Vec::with_capacity(3);
for panel_state in &mut panels {
panel_state.render(&ctx)?;
frames.push(
panel_state
.capture
.as_mut()
.and_then(CaptureDump::take_rgba)
.ok_or_else(|| anyhow::anyhow!("missing panel pixels"))?,
);
}
let stacked = common::hstack_rgba(&[&frames[0], &frames[1], &frames[2]], panel.0, panel.1)?;
output.write_rgba(&stacked)?;
}
Ok(())
}
fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn")),
)
.init();
if common::capture_requested() {
return capture_panels();
}
println!("Goldy Multi-Window Example (Scheme + Present)");
println!("Three windows, three effects, independent controls:");
println!();
println!(" Plasma: Space=pause Click/R=reset");
println!(" Tunnel: Space=reverse Click/R=reset");
println!(" Starfield: Space=warp Click/R=reset");
println!();
println!("Escape closes the focused window. Close all to exit.");
println!();
let event_loop = EventLoop::new()?;
event_loop.set_control_flow(ControlFlow::Poll);
event_loop.run_app(&mut App::new()?)?;
Ok(())
}
The example pulls in examples/common.rs — see Shared Helpers.
Shaders
shaders/starfield.slang:
// 3D Starfield - flying forward through space
// Uses vertex time attribute for animation (same pattern as plasma/tunnel)
struct VertexInput {
float2 position : POSITION;
float2 uv : TEXCOORD0;
float time : TEXCOORD1;
};
struct VertexOutput {
float4 position : SV_Position;
float2 uv : TEXCOORD0;
float time : TEXCOORD1;
};
[goldy_vertex]
VertexOutput vs_main(VertexInput input) {
VertexOutput output;
output.position = float4(input.position, 0.0, 1.0);
output.uv = input.uv;
output.time = input.time;
return output;
}
float hash(float p) {
return frac(sin(p * 127.1) * 43758.5453);
}
[goldy_fragment]
float4 fs_main(VertexOutput input) : SV_Target {
float2 uv = (input.uv - 0.5) * 2.0;
float t = input.time;
float3 color = float3(0.0, 0.0, 0.02);
int num_stars = 200;
float speed = 0.3;
for (int i = 0; i < num_stars; i++) {
float fi = float(i);
// Random angle for each star
float angle = hash(fi) * 6.28318;
// Random max distance (how far from center it can go)
float max_dist = 0.3 + hash(fi + 50.0) * 1.2;
// All stars cycle at same speed, just different phases
float phase = hash(fi + 100.0);
float cycle = frac(t * speed + phase);
// Distance from center increases as cycle goes 0->1
float dist = max_dist * cycle;
float star_x = cos(angle) * dist;
float star_y = sin(angle) * dist;
float pixel_dist = length(uv - float2(star_x, star_y));
// Size increases with distance (closer = bigger)
float size = 0.002 + cycle * 0.015;
// Brightness increases with distance
float brightness = cycle * smoothstep(size, 0.0, pixel_dist);
// Fade in at spawn
float fade = smoothstep(0.0, 0.1, cycle);
color += float3(0.9, 0.95, 1.0) * brightness * fade;
}
return float4(clamp(color, float3(0.0, 0.0, 0.0), float3(1.0, 1.0, 1.0)), 1.0);
}