Render Loop and HTTP Server
Render loop with wgpu
The 07-render-loop demo shows a winit event loop that renders 1000 cubes in a
10×10×10 grid with GPU instancing. The separate config.zolo module exposes
get_clear_color() — just edit it and save while the window is open to see the
background color change on the next frame, without reinitializing the pipeline.
cd examples/features/21-hot-reload/07-render-loop
zolo dev main.zolo
Change the r/g/b values here to alter the background color in real time.
pub fn get_clear_color() {
return #{r: 0.1, g: 0.02, b: 0.05, a: 1.0}
}
Requires the Zolo CLI/host — open in the playground or run locally.
winit + wgpu event loop: init_gpu(), render(), and on_event() with match.
/// 1,000 Rotating Cubes — winit + wgpu
///
/// Demonstrates instanced rendering of 1K cubes in a 10×10×10 grid,
/// each with unique rotation. Uses GPU instancing via instance_index.
use std::os
use plugin winit::*
use plugin wgpu::*
use plugin wgpu
use config::{get_clear_color}
let win = Window.new(#{title: "Zolo — 1K Cubes", width: 800, height: 600})
let event_loop = EventLoop.new()
// GPU state — initialized on first redraw (after window is realized)
var gpu_ready = false
var adapter_h = nil
var device_h = nil
var queue_h = nil
var surface_h = nil
var pipeline_h = nil
var bind_group_h = nil
var uniform_buf_h = nil
var depth_view_h = nil
var frame_count = 0
var win_w = 800
var win_h = 600
// FPS counter
var fps_last_time = os.clock()
var fps_frames = 0
var fps_current = 0
// Frame pacing (144 FPS target)
const TARGET_FRAME_TIME = 1.0 / 144.0
var frame_start = os.clock()
const shader_src = /*wgsl*/ r"
struct Uniforms { time: f32, aspect: f32 }
@group(0) @binding(0) var<uniform> u: Uniforms;
struct VsOut {
@builtin(position) pos: vec4f,
@location(0) color: vec3f,
}
@vertex
fn vs_main(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VsOut {
var p = array<vec3f, 36>(
vec3f(-1,-1, 1), vec3f( 1,-1, 1), vec3f( 1, 1, 1),
vec3f(-1,-1, 1), vec3f( 1, 1, 1), vec3f(-1, 1, 1),
vec3f( 1,-1,-1), vec3f(-1,-1,-1), vec3f(-1, 1,-1),
vec3f( 1,-1,-1), vec3f(-1, 1,-1), vec3f( 1, 1,-1),
vec3f( 1,-1, 1), vec3f( 1,-1,-1), vec3f( 1, 1,-1),
vec3f( 1,-1, 1), vec3f( 1, 1,-1), vec3f( 1, 1, 1),
vec3f(-1,-1,-1), vec3f(-1,-1, 1), vec3f(-1, 1, 1),
vec3f(-1,-1,-1), vec3f(-1, 1, 1), vec3f(-1, 1,-1),
vec3f(-1, 1, 1), vec3f( 1, 1, 1), vec3f( 1, 1,-1),
vec3f(-1, 1, 1), vec3f( 1, 1,-1), vec3f(-1, 1,-1),
vec3f(-1,-1,-1), vec3f( 1,-1,-1), vec3f( 1,-1, 1),
vec3f(-1,-1,-1), vec3f( 1,-1, 1), vec3f(-1,-1, 1),
);
var c = array<vec3f, 6>(
vec3f(0.3, 0.5, 1.0),
vec3f(1.0, 0.3, 0.3),
vec3f(0.3, 1.0, 0.3),
vec3f(1.0, 1.0, 0.3),
vec3f(0.3, 1.0, 1.0),
vec3f(1.0, 0.3, 1.0),
);
// 10x10x10 grid — decode instance index into 3D grid position
let ix = f32(ii % 10u);
let iy = f32((ii / 10u) % 10u);
let iz = f32(ii / 100u);
// Grid offset: center the grid, spacing of 3.0 units
let spacing = 3.0;
let grid_offset = vec3f(
(ix - 4.5) * spacing,
(iy - 4.5) * spacing,
(iz - 4.5) * spacing,
);
// Per-instance rotation speed variation using instance index
let seed = f32(ii) * 0.0037;
let t = u.time + seed;
let v = p[vi] * 0.8;
let cy = cos(t); let sy = sin(t);
let cx = cos(t * 0.7); let sx = sin(t * 0.7);
let ry = vec3f(v.x*cy + v.z*sy, v.y, -v.x*sy + v.z*cy);
let r = vec3f(ry.x, ry.y*cx - ry.z*sx, ry.y*sx + ry.z*cx);
// Translate to grid position
let local = r + grid_offset;
// Global rotation of the entire matrix
let gt = u.time * 0.3;
let gcy = cos(gt); let gsy = sin(gt);
let gcx = cos(gt * 0.5); let gsx = sin(gt * 0.5);
let gry = vec3f(local.x*gcy + local.z*gsy, local.y, -local.x*gsy + local.z*gcy);
let world = vec3f(gry.x, gry.y*gcx - gry.z*gsx, gry.y*gsx + gry.z*gcx);
let z = world.z + 60.0;
let fov = 2.0;
// Color tint based on grid position
let tint = vec3f(ix / 9.0, iy / 9.0, iz / 9.0) * 0.6 + 0.4;
var out: VsOut;
out.pos = vec4f(world.x * fov / (z * u.aspect), world.y * fov / z, (world.z + 50.0) / 150.0, 1.0);
out.color = c[vi / 6u] * tint;
return out;
}
@fragment
fn fs_main(@location(0) color: vec3f) -> @location(0) vec4f {
return vec4f(color, 1.0);
}
"
fn init_gpu() {
let hwnd = win.native_handle()
// Create surface FIRST, then request adapter compatible with it
surface_h = wgpu.create_surface(hwnd)
adapter_h = wgpu.request_adapter(#{power_preference: "high", compatible_surface: surface_h})
let dq = adapter_h.request_device(#{})
device_h = dq.device
queue_h = dq.queue
surface_h.configure(adapter_h, device_h, win_w, win_h)
let fmt = surface_h.format()
let shader = device_h.create_shader(#{label: "cube", source: shader_src})
// Uniform buffer (16 bytes = 1 float + padding, aligned to 16)
pipeline_h = device_h.create_render_pipeline(#{
label: "cube-pipeline",
vertex_shader: shader,
vertex_entry: "vs_main",
fragment_shader: shader,
fragment_entry: "fs_main",
color_format: fmt,
depth_format: "depth32float",
})
uniform_buf_h = device_h.create_buffer(#{
label: "uniforms",
size: 16,
usage: "uniform|copy_dst",
})
let bgl = pipeline_h.get_bind_group_layout(0)
bind_group_h = device_h.create_bind_group(bgl, uniform_buf_h, 0)
depth_view_h = device_h.create_depth_view(win_w, win_h)
gpu_ready = true
print("GPU initialized: {fmt}")
}
fn render() {
frame_start = os.clock()
frame_count = frame_count + 1
let t = frame_count * 0.016
// FPS counter — update every second
fps_frames = fps_frames + 1
let now = os.clock()
if now - fps_last_time >= 1.0 {
fps_current = fps_frames
fps_frames = 0
fps_last_time = now
print("FPS: {fps_current}")
}
if frame_count == 1 {
print("First render frame, t={t}")
}
let aspect = win_w * 1.0 / (win_h * 1.0)
queue_h.write_floats(uniform_buf_h, 0, [t, aspect, 0.0, 0.0])
let tex = surface_h.get_current_texture()
let view = tex.create_view()
let enc = device_h.create_command_encoder(#{label: "frame"})
let pass = enc.begin_render_pass(#{
color_attachments: #{
"1": #{
view,
load: "clear",
clear_color: get_clear_color(),
store: "store",
},
},
depth_view: depth_view_h,
})
sleep 16ms
pass.set_pipeline(pipeline_h)
pass.set_bind_group(0, bind_group_h)
pass.draw(36, 1000, 0, 0)
pass.end()
let cmd = enc.finish()
queue_h.submit(#{"1": cmd})
view.destroy()
tex.present()
// Busy-wait for frame pacing precision
// while os.clock() - frame_start < TARGET_FRAME_TIME {
// sleep 1ms
// }
}
fn on_event(event) {
match event.event {
"redraw_requested" => {
if !gpu_ready {
init_gpu()
}
render()
},
"resized" => {
if gpu_ready {
let w = event.width
let h = event.height
if w > 0 && h > 0 {
win_w = w
win_h = h
surface_h.configure(adapter_h, device_h, win_w, win_h)
depth_view_h = device_h.create_depth_view(win_w, win_h)
}
}
},
"close_requested" => {
print("Goodbye!")
},
_ => { },
}
}
event_loop.run(on_event)
Requires the Zolo CLI/host — open in the playground or run locally.
HTTP server without restarting
The 10-http-server demo starts a real server on port 8080. The handlers.zolo
module defines the home and hello routes. Editing and saving the handlers
while the server is running replaces the functions via swap; the next request
already uses the new version — the listening socket is not closed.
cd examples/features/21-hot-reload/10-http-server
zolo dev main.zolo
Then, in another terminal:
curl http://localhost:8080/
curl http://localhost:8080/hello/zolo
Edit the "body" field and curl again — the response changes immediately.
// Edit these handlers while the server is running. Each save triggers
// a hot-swap; the next request uses the new code.
pub fn home(req) {
return #{
"status": 200,
"body": "<h1>Zolo Live</h1><p>Edit handlers.zolo and refresh.</p>",
}
}
pub fn hello(req) {
let name = req.params.name
return #{
"status": 200,
"body": "Hi, {name}! (V1)",
}
}
Requires the Zolo CLI/host — open in the playground or run locally.
// Demo 06 — Hot-reload an HTTP server's route handlers.
//
// zolo dev examples/zolo_live/06_http_server/main.zolo
//
// Starts a real HTTP server on port 8080. Edit handlers.zolo while the
// server is running; the VM instruction hook drains pending swaps
// between requests, so the next incoming request hits the new handler
// — without restarting the server, dropping the listener socket, or
// losing in-flight state.
//
// Test from another terminal:
// curl http://localhost:8080/
// curl http://localhost:8080/hello/world
//
// Then edit handlers.zolo and re-run the curl: see the new response.
use std::http::{Server, Router}
use handlers::{home, hello}
fn main() {
let router = Router.new()
router.get("/", home)
router.get("/hello/:name", hello)
let server = Server.new("0.0.0.0:8080", router)
print("Listening on http://localhost:8080 (edit handlers.zolo to hot-reload)")
server.run()
}
Requires the Zolo CLI/host — open in the playground or run locally.
Challenge
In handlers.zolo, add a new route pub fn about(req) that returns version
information. Then, in main.zolo, register router.get("/about", about).
Save both files and verify with curl http://localhost:8080/about
that the route appears without restarting the server.