///|
extern "C" fn native_engine_clock_now_ms() -> Double = "kagura_runtime_now_ms"
///|
pub fn run(
update~ : (@core.InputSnapshot) -> Unit,
draw~ : (EngineContext) -> Array[@gfx.DrawTrianglesCommand],
on_frame? : () -> Unit,
after_render? : (Double, Double) -> Unit,
audio_ctx? : @audio.MixerAudioContext,
audio_frames_per_tick? : Int = 735,
width? : Int = 320,
height? : Int = 240,
title? : String = "kagura",
canvas? : String = "",
) -> Unit {
let _ = canvas
invoke_on_start(canvas, title)
let platform = @platform.create_desktop_glfw_platform()
let surface = platform.current_surface() catch {
_ => @gfx.create_offscreen_surface_token(width, height)
}
let graphics = @gfx.create_wgpu_native_graphics(
surface,
@gfx.default_graphics_backend_options(),
)
let window_options = @platform.new_window_options(
title, width, height, false, true,
)
platform.initialize(window_options) catch {
e => println("[engine] platform.initialize failed: \{e}")
}
graphics.initialize() catch {
e => println("[engine] graphics.initialize failed: \{e}")
}
let shader_source = @gfx.default_builtin_shader_source()
let dst = graphics.new_image(width, height) catch {
e => {
println("[engine] graphics.new_image failed: \{e}")
@gfx.new_image_handle(1, width, height)
}
}
let shader = graphics.new_shader(shader_source) catch {
e => {
println("[engine] graphics.new_shader failed: \{e}")
@gfx.new_shader_handle(1, shader_source)
}
}
let ctx : EngineContext = { dst, shader, screen_w: width, screen_h: height }
let mut tick = 0
match audio_ctx {
Some(_) => {
let _ = @audio.audio_try_initialize(@audio.default_audio_format())
}
None => ()
}
// Audio accumulator: tick audio at fixed intervals independent of frame rate
let audio_tick_ms = audio_frames_per_tick.to_double() / 44100.0 * 1000.0
let mut audio_accum = 0.0
let mut prev_time = native_engine_clock_now_ms()
while !platform.should_close() {
platform.poll_events()
// Resize tracking
let outside = platform.outside_size()
let new_w = outside.width.to_int()
let new_h = outside.height.to_int()
if new_w > 0 &&
new_h > 0 &&
(new_w != ctx.screen_w || new_h != ctx.screen_h) {
graphics.resize(new_w, new_h) catch {
e => println("[engine] graphics.resize failed: \{e}")
}
ctx.screen_w = new_w
ctx.screen_h = new_h
ctx.dst = graphics.new_image(new_w, new_h) catch {
e => {
println("[engine] graphics.new_image (resize) failed: \{e}")
@gfx.new_image_handle(ctx.dst.id, new_w, new_h)
}
}
}
let input = platform.capture_input(tick)
tick = tick + 1
update(input)
match on_frame {
Some(f) => f()
None => ()
}
let draw_callback_start = native_engine_clock_now_ms()
let cmds = draw(ctx)
let draw_callback_end = native_engine_clock_now_ms()
let render_start = native_engine_clock_now_ms()
render_commands(graphics, cmds) catch {
e => println("[engine] render_commands failed: \{e}")
}
let render_end = native_engine_clock_now_ms()
match after_render {
Some(f) =>
f(draw_callback_end - draw_callback_start, render_end - render_start)
None => ()
}
// Time-based audio ticking
let now = native_engine_clock_now_ms()
let elapsed = now - prev_time
prev_time = now
match audio_ctx {
Some(actx) => {
audio_accum = audio_accum + elapsed
while audio_accum >= audio_tick_ms {
let _ = @audio.audio_render_and_write(actx, audio_frames_per_tick)
actx.tick_bgm(audio_frames_per_tick)
audio_accum = audio_accum - audio_tick_ms
}
}
None => ()
}
}
invoke_on_stop()
}