Skip to content

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.

config.zolo
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.

main.zolo
/// 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.

handlers.zolo
// 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.

main.zolo
// 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.

enespt-br