Real-Time Presentation (flow.present)
Traditional graphics programming enforces a strict separation between compute pipelines and rendering pipelines. Drawing pixels on screen typically requires setting up render passes, vertex buffers, rasterizers, and fragment shaders.
Enki unifies this through direct buffer presentation: you write pixels into a standard contiguous VRAM buffer (GpuVec<u32>) using a compute nam, and queue it directly to the display window’s swapchain in a single statement.
1. Adding Dependencies
To build a real-time windowed application, add enki-gpu and glfw to your project:
cargo add enki-gpu glfw
Your Cargo.toml dependencies should include:
[dependencies]
enki-gpu = "0.1"
glfw = "0.59"
2. Window Setup & The NoApi Invariant
Vulkan manages swapchain images directly through native platform display surfaces. Therefore, you must explicitly instruct GLFW not to create an OpenGL context:
#![allow(unused)]
fn main() {
let mut glfw = glfw::init(glfw::fail_on_errors).expect("Failed to initialize GLFW");
// MANDATORY: Disable OpenGL context creation
glfw.window_hint(glfw::WindowHint::ClientApi(glfw::ClientApiHint::NoApi));
glfw.window_hint(glfw::WindowHint::Resizable(true));
let (mut window, events) = glfw
.create_window(WIDTH, HEIGHT, "Enki Display", glfw::WindowMode::Windowed)
.expect("Failed to create GLFW window");
// Enable polling for resize and keyboard events
window.set_framebuffer_size_polling(true);
window.set_key_polling(true);
}
Passing the Window to Enki
Enki’s windowed initializer expects an atomic reference counted (Arc<W>) window instance implementing raw_window_handle::HasWindowHandle and HasDisplayHandle.
Because GLFW’s Window implements these traits natively, pass it directly:
#![allow(unused)]
fn main() {
let window_handle = Arc::new(window);
// Initialize Enki in windowed mode
let enki = Enki::init_windowed(window_handle.clone(), WIDTH, HEIGHT);
}
3. The Complete, Runnable Application
Below is a complete, self-contained src/main.rs that renders an animated real-time procedural color plasma at 60 FPS and handles dynamic window resizing cleanly without mutable borrowing conflicts:
use enki::*;
use glfw::{Action, Context, Key, WindowEvent, WindowHint, WindowMode};
use std::sync::Arc;
use std::time::Instant;
const WIDTH: u32 = 1280;
const HEIGHT: u32 = 720;
// 1. Declare the compute nam that generates pixel colors
#[nam]
fn render_plasma(space: &Space, pixel: &mut u32, time: f32) {
if !space.in_bounds_xy() {
return;
}
let (u, v) = space.uv();
// Compute animated color waves
let r = ((u * 10.0 + time).sin() * 0.5 + 0.5);
let g = ((v * 10.0 - time * 1.5).cos() * 0.5 + 0.5);
let b = (((u + v) * 8.0 + time * 2.0).sin() * 0.5 + 0.5);
// Pack RGB floats into 32-bit presentation integer (0xAARRGGBB)
*pixel = space.set_rgb_color(r, g, b);
}
fn main() {
// 2. Initialize GLFW with NoApi
let mut glfw = glfw::init(glfw::fail_on_errors).expect("Failed to initialize GLFW");
glfw.window_hint(WindowHint::ClientApi(glfw::ClientApiHint::NoApi));
glfw.window_hint(WindowHint::Resizable(true));
let mut current_width = WIDTH;
let mut current_height = HEIGHT;
let (mut window, events) = glfw
.create_window(current_width, current_height, "Enki Plasma", WindowMode::Windowed)
.expect("Failed to create window");
window.set_framebuffer_size_polling(true);
window.set_key_polling(true);
let window_handle = Arc::new(window);
// 3. Initialize Enki windowed runtime
let enki = Enki::init_windowed(window_handle.clone(), current_width, current_height);
// 4. Allocate the physical VRAM framebuffer
let mut screen = gpu_vec![0u32; (current_width * current_height) as usize];
let start_time = Instant::now();
let mut is_running = true;
// 5. Main presentation loop
while is_running && !window_handle.should_close() {
glfw.poll_events();
for (_, event) in glfw::flush_messages(&events) {
match event {
WindowEvent::Close => is_running = false,
WindowEvent::Key(Key::Escape, _, Action::Press, _) => is_running = false,
_ => {}
}
}
// Handle dynamic swapchain recreation if window dimensions changed
let (fb_w, fb_h) = window_handle.get_framebuffer_size();
if fb_w > 0 && fb_h > 0 && (fb_w as u32 != current_width || fb_h as u32 != current_height) {
current_width = fb_w as u32;
current_height = fb_h as u32;
// Recreate Vulkan swapchain images
let _ = enki.resize(current_width, current_height);
// Reallocate VRAM screen buffer to match new extent
screen = GpuVec::zeroed((current_width * current_height) as usize);
}
let time = start_time.elapsed().as_secs_f32();
// 6. Record and submit the compute-to-display flow
enki.flow(|flow| {
render_plasma.run(
&Space::gpu_xy(current_width as usize, current_height as usize),
&mut screen,
GpuParam::new(time),
);
// Queue buffer for direct presentation
flow.present(&screen);
});
}
}
Run the application:
cargo run
A window will appear rendering an animated procedural plasma directly from the GPU compute cores at your monitor’s native refresh rate.
4. The Presentation Lifecycle
When flow.present(&screen) is recorded:
Host CPU Flow Recording
┌──────────────────────────────────────┐
│ render_plasma.run(&space, &mut screen) │ ──► Compute writes pixels into VRAM
├──────────────────────────────────────┤
│ flow.present(&screen) │ ──► Schedules hardware copy: Buffer ──► Swapchain Image
└──────────────────────────────────────┘
│
▼ (Queue Submission)
Vulkan Hardware Timeline
──[Compute Shader Finishes]──► [vkCmdCopyBufferToImage] ──► [vkQueuePresentKHR] ──► Display
- Copy Command: The engine injects an asynchronous transfer command (
vkCmdCopyBufferToImage) from theGpuVecinto the current swapchain image. - Synchronization: The engine automatically coordinates the timeline semaphore with the swapchain’s image-acquired and render-finished binary semaphores.
- Atomic Display: The frame is presented via
vkQueuePresentKHRonce memory transfers complete.
5. Dimension Verification (error[E1008])
Swapchain presentation requires an exact 1:1 pixel mapping. Attempting to present a buffer that contains fewer elements than the active window extent (width * height) halts execution immediately:
error[E1008]: GpuVec capacity is smaller than the requested space domain
--> src/main.rs:88:27
|
88 | render_plasma.run(
| ^^^^ container contains fewer elements than required space cells
|
= note: each cell in `space` expects exclusive 1:1 access to its
corresponding element; extra threads would access
out-of-bounds memory.
= note: argument 1 (`GpuVec<u32>`) contains only 1439999 elements
= note: space execution (dimensions: 1600x900x1) requires at least
1440000 elements (deficit of 1 elements)
= help: resize the `GpuVec` using `.resize(...)` to cover all space
cells, or adjust the space domain dimensions.