///|
let runtime_title : Ref[String] = Ref("kagura")
///|
priv struct SyncedSourceGeneration {
image_id : Int
generation : Int
}
///|
let synced_source_generations : Ref[Array[SyncedSourceGeneration]] = Ref([])
///|
let saw_native_touch_input : Ref[Bool] = Ref(false)
///|
let mouse_touch_fallback_enabled : Ref[Bool] = Ref(false)
///|
fn normalized_source_generation(generation : Int) -> Int {
if generation <= 0 {
1
} else {
generation
}
}
///|
fn synced_source_generation_at(image_id : Int) -> Int {
let mut generation = 0
for entry in synced_source_generations.val {
if entry.image_id == image_id {
generation = entry.generation
}
}
generation
}
///|
fn set_synced_source_generation(image_id : Int, generation : Int) -> Unit {
let next : Array[SyncedSourceGeneration] = []
let mut replaced = false
let safe_generation = normalized_source_generation(generation)
for entry in synced_source_generations.val {
if entry.image_id == image_id {
next.push({ image_id, generation: safe_generation, })
replaced = true
} else {
next.push(entry)
}
}
if !replaced {
next.push({ image_id, generation: safe_generation, })
}
synced_source_generations.val = next
}
///|
fn clear_synced_source_generations() -> Unit {
synced_source_generations.val = []
}
///|
fn reset_touch_capture_state() -> Unit {
saw_native_touch_input.val = false
mouse_touch_fallback_enabled.val = false
}
///|
fn bridge_desktop_set_mouse_touch_fallback(
active : Bool,
enabled : Bool,
) -> Unit {
if active {
mouse_touch_fallback_enabled.val = enabled
}
}
///|
fn bridge_desktop_mouse_touch_fallback_enabled(active : Bool) -> Bool {
if active {
mouse_touch_fallback_enabled.val
} else {
false
}
}
///|
fn drop_synced_source_generation(image_id : Int) -> Unit {
let next : Array[SyncedSourceGeneration] = []
for entry in synced_source_generations.val {
if entry.image_id != image_id {
next.push(entry)
}
}
synced_source_generations.val = next
}
///|
fn bridge_desktop_try_initialize(options : @platform.WindowOptions) -> Bool {
@wgpu_native.ensure_runtime_context(
options.width,
options.height,
options.title,
)
}
///|
fn bridge_desktop_poll(active : Bool) -> Unit {
if active {
@wgpu_native.runtime_poll_events()
}
}
///|
fn bridge_desktop_should_close() -> Bool {
@wgpu_native.runtime_should_close()
}
///|
fn bridge_desktop_outside_size(width : Int, height : Int) -> @core.OutsideSize {
let (w, h) = @wgpu_native.runtime_window_size(width, height)
@core.new_outside_size(w.to_double(), h.to_double())
}
///|
fn bridge_desktop_capture_input(
active : Bool,
_tick : Int,
) -> @core.InputSnapshot {
if active {
let cursor_x = @wgpu_native.runtime_cursor_x()
let cursor_y = @wgpu_native.runtime_cursor_y()
let wheel_x = @wgpu_native.runtime_take_wheel_x()
let wheel_y = @wgpu_native.runtime_take_wheel_y()
let pressed_keys : Array[Int] = []
let key_count = @wgpu_native.runtime_pressed_key_count()
for i in 0..= 0 {
pressed_keys.push(key)
}
}
let pressed_mouse_buttons : Array[Int] = []
let mouse_button_count = @wgpu_native.runtime_pressed_mouse_button_count()
for i in 0..= 0 {
pressed_mouse_buttons.push(button)
}
}
let touches : Array[@core.TouchPoint] = []
let touch_count = @wgpu_native.runtime_touch_count()
for i in 0..= 0 {
let touch_type = @wgpu_native.runtime_touch_type_at(i)
touches.push(
@core.new_touch_point_with_source(
touch_id,
@wgpu_native.runtime_touch_x_at(i),
@wgpu_native.runtime_touch_y_at(i),
@core.touch_source_from_int(touch_type),
),
)
}
}
let resolved_touches = resolve_touch_points_with_mouse_fallback(
touches,
pressed_mouse_buttons,
cursor_x,
cursor_y,
saw_native_touch_input.val,
mouse_touch_fallback_enabled.val,
)
saw_native_touch_input.val = resolved_touches.saw_native_touch
let gamepads : Array[@core.GamepadSnapshot] = []
let gamepad_count = @wgpu_native.runtime_gamepad_count()
for i in 0..= 0 {
let axes : Array[Double] = []
let axis_count = @wgpu_native.runtime_gamepad_axis_count(i)
for j in 0..= 0 {
pressed_buttons.push(button)
}
}
gamepads.push(
@core.new_gamepad_snapshot(gamepad_id, axes, pressed_buttons),
)
}
}
@core.new_input_snapshot_full(
cursor_x,
cursor_y,
wheel_x,
wheel_y,
pressed_keys,
pressed_mouse_buttons,
resolved_touches.touches,
gamepads,
)
} else {
@core.empty_input_snapshot()
}
}
///|
fn bridge_desktop_set_fullscreen(active : Bool, enabled : Bool) -> Bool {
if active {
@wgpu_native.runtime_set_fullscreen(enabled)
} else {
enabled
}
}
///|
fn bridge_desktop_is_fullscreen(active : Bool, current : Bool) -> Bool {
if active {
@wgpu_native.runtime_is_fullscreen()
} else {
current
}
}
///|
fn bridge_desktop_set_cursor_mode(
active : Bool,
mode : @platform.CursorMode,
) -> @platform.CursorMode {
if active {
@platform.cursor_mode_from_int(
@wgpu_native.runtime_set_cursor_mode(@platform.cursor_mode_to_int(mode)),
)
} else {
mode
}
}
///|
fn bridge_desktop_cursor_mode(
active : Bool,
current : @platform.CursorMode,
) -> @platform.CursorMode {
if active {
@platform.cursor_mode_from_int(@wgpu_native.runtime_cursor_mode())
} else {
current
}
}
///|
fn bridge_desktop_set_device_scale_factor(
active : Bool,
scale : Double,
) -> Double {
if active {
@wgpu_native.runtime_set_device_scale_factor(scale)
} else if scale <= 0.0 {
1.0
} else {
scale
}
}
///|
fn bridge_desktop_device_scale_factor(
active : Bool,
current : Double,
) -> Double {
if active {
@wgpu_native.runtime_device_scale_factor(current)
} else if current <= 0.0 {
1.0
} else {
current
}
}
///|
fn bridge_desktop_set_vsync_enabled(active : Bool, enabled : Bool) -> Bool {
if active {
@wgpu_native.runtime_set_vsync_enabled(enabled)
} else {
enabled
}
}
///|
fn bridge_desktop_is_vsync_enabled(active : Bool, current : Bool) -> Bool {
if active {
@wgpu_native.runtime_is_vsync_enabled()
} else {
current
}
}
///|
fn bridge_desktop_close_window(active : Bool) -> Unit {
if active {
@wgpu_native.runtime_request_close()
}
}
///|
fn bridge_desktop_request_attention(active : Bool) -> Unit {
if active {
@wgpu_native.runtime_request_attention()
}
}
///|
fn bridge_gfx_try_initialize(width : Int, height : Int) -> Bool {
@wgpu_native.ensure_runtime_context(width, height, runtime_title.val)
}
///|
fn bridge_gfx_on_begin(active : Bool, pass : @gfx.RenderPassDesc) -> Unit {
if active {
@wgpu_native.runtime_begin_frame(
pass.clear_color.r,
pass.clear_color.g,
pass.clear_color.b,
pass.clear_color.a,
)
}
}
///|
fn bridge_gfx_on_end(active : Bool, present : Bool) -> Unit {
if active && present {
let _ = @wgpu_native.runtime_present_planned_frame()
}
}
///|
fn bridge_gfx_on_draw(
active : Bool,
command : @gfx.DrawTrianglesCommand,
) -> Unit {
if active {
let dispatch = @gfx.DrawTrianglesCommand::build_dispatch(command)
// Split draw command into individual triangles (3 indices each).
// IMPORTANT: Set payload BEFORE pushing command, because
// runtime_record_draw_command captures the current payload state
// via planned_has_triangle_payload at push time.
let indices = command.indices
let tri_count = indices.length() / 3
for t = 0; t < tri_count; t = t + 1 {
// Push command first (has_payload=false by default)
@wgpu_native.runtime_record_draw_command(
1,
dispatch.pipeline_id,
dispatch.uniform_hash,
dispatch.blend_mode,
dispatch.dst_image_id,
dispatch.shader_id,
dispatch.index_offset,
1,
3,
dispatch.vertex_float_count,
3,
dispatch.src_image_count,
dispatch.uniform_dword_count,
)
// Then payload updates last command to has_payload=true with vertex data
let base = t * 3
let sub_indices = [indices[base], indices[base + 1], indices[base + 2]]
let payload = @renderer2d.decode_triangle_payload(
command.vertex_data,
sub_indices,
command.uniform_dwords,
command.src_image_ids,
)
if payload.has_payload {
@wgpu_native.runtime_record_draw_triangle_payload(
payload.ax,
payload.ay,
payload.bx,
payload.by,
payload.cx,
payload.cy,
payload.au,
payload.av,
payload.bu,
payload.bv,
payload.cu,
payload.cv,
payload.uniform_r,
payload.uniform_g,
payload.uniform_b,
payload.uniform_a,
payload.texture_seed,
)
}
}
}
}
///|
fn bridge_gfx_on_resize(active : Bool, width : Int, height : Int) -> Unit {
if active {
let _ = @wgpu_native.runtime_resize_surface(width, height)
}
}
///|
priv struct OffscreenImage {
id : Int
width : Int
height : Int
}
///|
let last_offscreen_image : Ref[OffscreenImage?] = Ref(None)
///|
fn bridge_gfx_on_read_pixels(
active : Bool,
x : Int,
y : Int,
width : Int,
height : Int,
) -> Array[Int]? {
if !active {
return None
}
match @wgpu_native.runtime_read_pixels(x, y, width, height) {
Some(pixels) => Some(pixels)
None =>
match last_offscreen_image.val {
None => None
Some(image) =>
@wgpu_native.runtime_read_offscreen_pixels(
image.id,
x,
y,
width,
height,
)
}
}
}
///|
fn bridge_gfx_on_new_image(
active : Bool,
image_id : Int,
width : Int,
height : Int,
) -> Unit {
if active {
@wgpu_native.register_offscreen_target(image_id, width, height)
last_offscreen_image.val = Some({ id: image_id, width, height, })
}
}
///|
fn palette_channel_or_zero(
palette : @atlas.ImagePalette2x2,
pixel_index : Int,
channel_index : Int,
) -> Int {
let value = @atlas.palette_channel(palette, pixel_index, channel_index)
if value < 0 {
0
} else {
value
}
}
///|
pub fn register_source_image_palette(
source_image_id : Int,
palette : @atlas.ImagePalette2x2,
) -> Unit {
@wgpu_native.runtime_register_source_image_palette(
source_image_id,
palette_channel_or_zero(palette, 0, 0),
palette_channel_or_zero(palette, 0, 1),
palette_channel_or_zero(palette, 0, 2),
palette_channel_or_zero(palette, 0, 3),
palette_channel_or_zero(palette, 1, 0),
palette_channel_or_zero(palette, 1, 1),
palette_channel_or_zero(palette, 1, 2),
palette_channel_or_zero(palette, 1, 3),
palette_channel_or_zero(palette, 2, 0),
palette_channel_or_zero(palette, 2, 1),
palette_channel_or_zero(palette, 2, 2),
palette_channel_or_zero(palette, 2, 3),
palette_channel_or_zero(palette, 3, 0),
palette_channel_or_zero(palette, 3, 1),
palette_channel_or_zero(palette, 3, 2),
palette_channel_or_zero(palette, 3, 3),
)
if source_image_id >= 0 {
drop_synced_source_generation(source_image_id)
}
}
///|
pub fn register_source_image_solid_color(
source_image_id : Int,
color : @atlas.Rgba,
) -> Unit {
register_source_image_palette(
source_image_id,
@atlas.new_solid_palette(color),
)
}
///|
pub fn register_source_image_rgba8(
source_image_id : Int,
width : Int,
height : Int,
rgba8_channels : Array[Int],
) -> Unit {
@wgpu_native.runtime_register_source_image_rgba8(
source_image_id, width, height, rgba8_channels,
)
if source_image_id >= 0 {
drop_synced_source_generation(source_image_id)
}
}
///|
pub fn patch_source_image_rgba8(
source_image_id : Int,
width : Int,
height : Int,
dirty_x : Int,
dirty_y : Int,
dirty_width : Int,
dirty_height : Int,
rgba8_channels : Array[Int],
) -> Bool {
@wgpu_native.runtime_patch_source_image_rgba8(
source_image_id, width, height, dirty_x, dirty_y, dirty_width, dirty_height,
rgba8_channels,
)
}
///|
pub fn clear_source_image_palettes() -> Unit {
@wgpu_native.runtime_clear_source_image_registry()
clear_synced_source_generations()
}
///|
pub fn sync_source_image_palettes(
bindings : Array[@atlas.ImagePaletteBinding],
) -> Int {
let mut count = 0
for binding in bindings {
register_source_image_palette(binding.image_id, binding.palette)
count = count + 1
}
count
}
///|
pub fn sync_source_image_palettes_from_repository(
repository : @atlas.SimpleImageRepository,
) -> Int {
sync_source_image_palettes(@atlas.list_image_palette_bindings(repository))
}
///|
pub fn sync_source_images(bindings : Array[@atlas.SourceImageBinding]) -> Int {
let mut count = 0
for binding in bindings {
if binding.image_id < 0 {
continue
}
let generation = normalized_source_generation(binding.generation)
if synced_source_generation_at(binding.image_id) == generation {
continue
}
let required_channels = binding.width * binding.height * 4
if required_channels > 0 &&
binding.pixels_rgba8.length() >= required_channels {
let mut patched = false
match binding.dirty_rect {
Some(rect) => {
let full_rect = rect.x <= 0 &&
rect.y <= 0 &&
rect.width >= binding.width &&
rect.height >= binding.height
if !full_rect {
patched = patch_source_image_rgba8(
binding.image_id,
binding.width,
binding.height,
rect.x,
rect.y,
rect.width,
rect.height,
binding.pixels_rgba8,
)
}
}
None => ()
}
if !patched {
register_source_image_rgba8(
binding.image_id,
binding.width,
binding.height,
binding.pixels_rgba8,
)
}
} else {
register_source_image_palette(binding.image_id, binding.palette)
}
set_synced_source_generation(binding.image_id, generation)
count = count + 1
}
count
}
///|
pub fn sync_source_images_from_repository(
repository : @atlas.SimpleImageRepository,
) -> Int {
sync_source_images(@atlas.list_source_image_bindings(repository))
}
///|
pub fn sync_dirty_source_images_from_repository(
repository : @atlas.SimpleImageRepository,
) -> Int {
let synced = sync_source_images(
@atlas.list_dirty_source_image_bindings(repository),
)
let _ = @atlas.clear_source_image_dirty_flags(repository)
synced
}
///|
pub fn sync_dirty_atlas_pages_from_repository(
repository : @atlas.SimpleAtlasImageRepository,
) -> Int {
let synced = sync_source_images(
@atlas.list_dirty_atlas_page_bindings(repository),
)
let _ = @atlas.clear_dirty_atlas_page_flags(repository)
synced
}
///|
pub fn install(title : String) -> Unit {
runtime_title.val = title
clear_synced_source_generations()
reset_touch_capture_state()
@platform.set_desktop_native_hooks(
@platform.new_desktop_native_hooks(
bridge_desktop_try_initialize, bridge_desktop_poll, bridge_desktop_should_close,
bridge_desktop_outside_size, bridge_desktop_capture_input, bridge_desktop_set_fullscreen,
bridge_desktop_is_fullscreen, bridge_desktop_set_cursor_mode, bridge_desktop_cursor_mode,
bridge_desktop_set_device_scale_factor, bridge_desktop_device_scale_factor,
bridge_desktop_set_vsync_enabled, bridge_desktop_is_vsync_enabled, bridge_desktop_close_window,
bridge_desktop_request_attention, bridge_desktop_set_mouse_touch_fallback,
bridge_desktop_mouse_touch_fallback_enabled,
),
)
@gfx.set_native_graphics_hooks(
@gfx.new_native_graphics_hooks_full(
bridge_gfx_try_initialize, bridge_gfx_on_begin, bridge_gfx_on_end, bridge_gfx_on_draw,
bridge_gfx_on_resize, bridge_gfx_on_read_pixels, bridge_gfx_on_new_image,
),
)
@text.set_font_load_hooks(@text.new_font_load_hooks(bridge_load_font_data))
@audio.set_audio_output_hooks(@wgpu_native.new_native_audio_output_hooks())
}
///|
fn bridge_load_font_data(name : String) -> Array[Int]? {
@wgpu_native.runtime_load_font_data(name)
}
///|
pub fn uninstall() -> Unit {
last_offscreen_image.val = None
clear_synced_source_generations()
reset_touch_capture_state()
@gfx.reset_native_graphics_hooks()
@platform.reset_desktop_native_hooks()
@text.reset_font_load_hooks()
@audio.reset_audio_output_hooks()
}
///|
pub fn is_ready() -> Bool {
@wgpu_native.has_runtime_context()
}
///|
pub fn shutdown() -> Unit {
@wgpu_native.shutdown_runtime_context()
}
///|
pub fn initialize_context(width : Int, height : Int, title : String) -> Bool {
install(title)
@wgpu_native.ensure_runtime_context(width, height, title)
}
///|
/// Initialize a headless context (hidden window) for offscreen rendering.
pub fn initialize_headless_context(width : Int, height : Int) -> Bool {
install("headless")
@wgpu_native.ensure_headless_runtime_context(width, height)
}
///|
pub fn should_close() -> Bool {
@wgpu_native.runtime_should_close()
}
///|
pub fn poll_events() -> Unit {
@wgpu_native.runtime_poll_events()
}
///|
pub fn render_frame() -> Bool {
@wgpu_native.runtime_render_frame()
}
///|
pub fn shutdown_context() -> Unit {
shutdown()
uninstall()
}
///|
/// Get an EngineContext for building draw commands in headless mode.
/// Must be called after initialize_headless_context().
pub fn get_engine_context(width : Int, height : Int) -> @engine.EngineContext {
// id=0 targets the swap chain surface (not an offscreen target)
let dst = @gfx.new_image_handle(0, width, height)
let shader = @gfx.new_shader_handle(1, @gfx.default_builtin_shader_source())
{ dst, shader, screen_w: width, screen_h: height, }
}
///|
/// Render a single frame from draw commands and present.
/// Records commands into the planned pipeline and renders using the
/// TriangleContext directly (bypasses runtime_present_planned_frame which
/// can fail with headless surface size queries).
pub fn render_frame_from_commands(
cmds : Array[@gfx.DrawTrianglesCommand],
clear_r : Double,
clear_g : Double,
clear_b : Double,
clear_a : Double,
) -> Bool {
// Record commands via bridge pipeline
bridge_gfx_on_begin(
true,
@gfx.new_render_pass_desc(
@gfx.new_color(clear_r, clear_g, clear_b, clear_a),
true,
),
)
for cmd in cmds {
bridge_gfx_on_draw(true, cmd)
}
// Use render_triangle_frame_with_staged_plan directly on the context
// instead of runtime_present_planned_frame (which calls
// ensure_runtime_surface_size and can fail in headless mode)
@wgpu_native.present_planned_frame_direct(clear_r, clear_g, clear_b, clear_a)
}
///|
fn init {
@host_services.install()
@engine.set_lifecycle_hooks({
on_start: fn(_canvas, title) { install(title) },
on_stop: fn() {
shutdown()
uninstall()
},
})
}