use std::cell::RefCell;
use std::path::PathBuf;
use std::rc::Rc;
use deno_core::OpState;
use crate::renderer::SpriteCommand;
use crate::renderer::TilemapStore;
use crate::renderer::PointLight;
use crate::renderer::camera::CameraBounds;
use crate::renderer::msdf::MsdfFontStore;
#[derive(Clone, Debug)]
pub enum BridgeAudioCommand {
LoadSound { id: u32, path: String },
StopAll,
SetMasterVolume { volume: f32 },
PlaySoundEx {
sound_id: u32,
instance_id: u64,
volume: f32,
looping: bool,
bus: u32,
pan: f32,
pitch: f32,
low_pass_freq: u32,
reverb_mix: f32,
reverb_delay_ms: u32,
},
PlaySoundSpatial {
sound_id: u32,
instance_id: u64,
volume: f32,
looping: bool,
bus: u32,
pitch: f32,
source_x: f32,
source_y: f32,
listener_x: f32,
listener_y: f32,
},
StopInstance { instance_id: u64 },
SetInstanceVolume { instance_id: u64, volume: f32 },
SetInstancePitch { instance_id: u64, pitch: f32 },
UpdateSpatialPositions {
updates: Vec<(u64, f32, f32)>, listener_x: f32,
listener_y: f32,
},
SetBusVolume { bus: u32, volume: f32 },
}
#[derive(Clone)]
pub struct RenderBridgeState {
pub sprite_commands: Vec<SpriteCommand>,
pub camera_x: f32,
pub camera_y: f32,
pub camera_zoom: f32,
pub camera_dirty: bool,
pub delta_time: f64,
pub elapsed_time: f64,
pub keys_down: std::collections::HashSet<String>,
pub keys_pressed: std::collections::HashSet<String>,
pub mouse_x: f32,
pub mouse_y: f32,
pub mouse_buttons_down: std::collections::HashSet<u8>,
pub mouse_buttons_pressed: std::collections::HashSet<u8>,
pub gamepad_buttons_down: std::collections::HashSet<String>,
pub gamepad_buttons_pressed: std::collections::HashSet<String>,
pub gamepad_axes: std::collections::HashMap<String, f32>,
pub gamepad_count: u32,
pub gamepad_name: String,
pub touch_points: Vec<(u64, f32, f32)>,
pub touch_count: u32,
pub texture_load_queue: Vec<(String, u32)>,
pub texture_load_queue_linear: Vec<(String, u32)>,
pub base_dir: PathBuf,
pub next_texture_id: u32,
pub texture_path_to_id: std::collections::HashMap<String, u32>,
pub tilemaps: TilemapStore,
pub ambient_light: [f32; 3],
pub point_lights: Vec<PointLight>,
pub audio_commands: Vec<BridgeAudioCommand>,
pub next_sound_id: u32,
pub sound_path_to_id: std::collections::HashMap<String, u32>,
pub font_texture_queue: Vec<u32>,
pub viewport_width: f32,
pub viewport_height: f32,
pub scale_factor: f32,
pub clear_color: [f32; 4],
pub save_dir: PathBuf,
pub shader_create_queue: Vec<(u32, String, String)>,
pub shader_param_queue: Vec<(u32, u32, [f32; 4])>,
pub next_shader_id: u32,
pub effect_create_queue: Vec<(u32, String)>,
pub effect_param_queue: Vec<(u32, u32, [f32; 4])>,
pub effect_remove_queue: Vec<u32>,
pub effect_clear: bool,
pub next_effect_id: u32,
pub camera_bounds: Option<CameraBounds>,
pub gi_enabled: bool,
pub gi_intensity: f32,
pub gi_probe_spacing: Option<f32>,
pub gi_interval: Option<f32>,
pub gi_cascade_count: Option<u32>,
pub emissives: Vec<[f32; 8]>,
pub occluders: Vec<[f32; 4]>,
pub directional_lights: Vec<[f32; 5]>,
pub spot_lights: Vec<[f32; 9]>,
pub msdf_fonts: MsdfFontStore,
pub msdf_builtin_queue: Vec<(u32, u32)>,
pub msdf_shader_queue: Vec<(u32, String)>,
pub msdf_shader_pool: Vec<u32>,
pub msdf_texture_load_queue: Vec<(String, u32)>,
pub raw_texture_upload_queue: Vec<(u32, u32, u32, Vec<u8>)>,
pub frame_time_ms: f64,
pub draw_call_count: usize,
}
impl RenderBridgeState {
pub fn new(base_dir: PathBuf) -> Self {
let save_dir = base_dir.join(".arcane").join("saves");
Self {
sprite_commands: Vec::new(),
camera_x: 0.0,
camera_y: 0.0,
camera_zoom: 1.0,
camera_dirty: false,
delta_time: 0.0,
elapsed_time: 0.0,
keys_down: std::collections::HashSet::new(),
keys_pressed: std::collections::HashSet::new(),
mouse_x: 0.0,
mouse_y: 0.0,
mouse_buttons_down: std::collections::HashSet::new(),
mouse_buttons_pressed: std::collections::HashSet::new(),
gamepad_buttons_down: std::collections::HashSet::new(),
gamepad_buttons_pressed: std::collections::HashSet::new(),
gamepad_axes: std::collections::HashMap::new(),
gamepad_count: 0,
gamepad_name: String::new(),
touch_points: Vec::new(),
touch_count: 0,
texture_load_queue: Vec::new(),
texture_load_queue_linear: Vec::new(),
base_dir,
next_texture_id: 1,
texture_path_to_id: std::collections::HashMap::new(),
tilemaps: TilemapStore::new(),
ambient_light: [1.0, 1.0, 1.0],
point_lights: Vec::new(),
audio_commands: Vec::new(),
next_sound_id: 1,
sound_path_to_id: std::collections::HashMap::new(),
font_texture_queue: Vec::new(),
viewport_width: 800.0,
viewport_height: 600.0,
scale_factor: 1.0,
clear_color: [0.1, 0.1, 0.15, 1.0],
save_dir,
shader_create_queue: Vec::new(),
shader_param_queue: Vec::new(),
next_shader_id: 1,
effect_create_queue: Vec::new(),
effect_param_queue: Vec::new(),
effect_remove_queue: Vec::new(),
effect_clear: false,
next_effect_id: 1,
camera_bounds: None,
gi_enabled: false,
gi_intensity: 1.0,
gi_probe_spacing: None,
gi_interval: None,
gi_cascade_count: None,
emissives: Vec::new(),
occluders: Vec::new(),
directional_lights: Vec::new(),
spot_lights: Vec::new(),
msdf_fonts: MsdfFontStore::new(),
msdf_builtin_queue: Vec::new(),
msdf_shader_queue: Vec::new(),
msdf_shader_pool: Vec::new(),
msdf_texture_load_queue: Vec::new(),
raw_texture_upload_queue: Vec::new(),
frame_time_ms: 0.0,
draw_call_count: 0,
}
}
}
#[deno_core::op2(fast)]
pub fn op_clear_sprites(state: &mut OpState) {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow_mut().sprite_commands.clear();
}
pub const SPRITE_STRIDE: usize = 22;
#[deno_core::op2(fast)]
pub fn op_submit_sprite_batch(state: &mut OpState, #[buffer] data: &[u8]) {
let floats: &[f32] = bytemuck::cast_slice(data);
let sprite_count = floats.len() / SPRITE_STRIDE;
let active_target = {
use super::target_ops::TargetState;
let ts = state.borrow::<Rc<RefCell<TargetState>>>();
ts.borrow().active_target
};
let parse_cmd = |s: &[f32]| SpriteCommand {
texture_id: s[0].to_bits(),
x: s[1],
y: s[2],
w: s[3],
h: s[4],
layer: s[5].to_bits() as i32,
uv_x: s[6],
uv_y: s[7],
uv_w: s[8],
uv_h: s[9],
tint_r: s[10],
tint_g: s[11],
tint_b: s[12],
tint_a: s[13],
rotation: s[14],
origin_x: s[15],
origin_y: s[16],
flip_x: s[17] != 0.0,
flip_y: s[18] != 0.0,
opacity: s[19],
blend_mode: (s[20] as u8).min(3),
shader_id: s[21].to_bits(),
};
if let Some(target_id) = active_target {
use super::target_ops::TargetState;
let ts = state.borrow::<Rc<RefCell<TargetState>>>();
let mut ts = ts.borrow_mut();
let queue = ts.target_sprite_queues.entry(target_id).or_default();
queue.reserve(sprite_count);
for i in 0..sprite_count {
let base = i * SPRITE_STRIDE;
queue.push(parse_cmd(&floats[base..base + SPRITE_STRIDE]));
}
} else {
let bridge = state.borrow::<Rc<RefCell<RenderBridgeState>>>();
let mut b = bridge.borrow_mut();
b.sprite_commands.reserve(sprite_count);
for i in 0..sprite_count {
let base = i * SPRITE_STRIDE;
b.sprite_commands.push(parse_cmd(&floats[base..base + SPRITE_STRIDE]));
}
}
}
#[deno_core::op2(fast)]
pub fn op_set_camera(state: &mut OpState, x: f64, y: f64, zoom: f64) {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
let mut b = bridge.borrow_mut();
b.camera_x = x as f32;
b.camera_y = y as f32;
b.camera_zoom = zoom as f32;
b.camera_dirty = true;
}
#[deno_core::op2]
#[serde]
pub fn op_get_camera(state: &mut OpState) -> Vec<f64> {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
let b = bridge.borrow();
vec![b.camera_x as f64, b.camera_y as f64, b.camera_zoom as f64]
}
#[deno_core::op2(fast)]
pub fn op_load_texture(state: &mut OpState, #[string] path: &str) -> u32 {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
let mut b = bridge.borrow_mut();
let resolved = if std::path::Path::new(path).is_absolute() {
path.to_string()
} else {
b.base_dir.join(path).to_string_lossy().to_string()
};
if let Some(&id) = b.texture_path_to_id.get(&resolved) {
return id;
}
let id = b.next_texture_id;
b.next_texture_id += 1;
b.texture_path_to_id.insert(resolved.clone(), id);
b.texture_load_queue.push((resolved, id));
id
}
#[deno_core::op2(fast)]
pub fn op_load_texture_linear(state: &mut OpState, #[string] path: &str) -> u32 {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
let mut b = bridge.borrow_mut();
let resolved = if std::path::Path::new(path).is_absolute() {
path.to_string()
} else {
b.base_dir.join(path).to_string_lossy().to_string()
};
if let Some(&id) = b.texture_path_to_id.get(&resolved) {
return id;
}
let id = b.next_texture_id;
b.next_texture_id += 1;
b.texture_path_to_id.insert(resolved.clone(), id);
b.texture_load_queue_linear.push((resolved, id));
id
}
#[deno_core::op2(fast)]
pub fn op_is_key_down(state: &mut OpState, #[string] key: &str) -> bool {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow().keys_down.contains(key)
}
#[deno_core::op2(fast)]
pub fn op_is_key_pressed(state: &mut OpState, #[string] key: &str) -> bool {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow().keys_pressed.contains(key)
}
#[deno_core::op2]
#[serde]
pub fn op_get_mouse_position(state: &mut OpState) -> Vec<f64> {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
let b = bridge.borrow();
vec![b.mouse_x as f64, b.mouse_y as f64]
}
#[deno_core::op2(fast)]
pub fn op_is_mouse_button_down(state: &mut OpState, button: u8) -> bool {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow().mouse_buttons_down.contains(&button)
}
#[deno_core::op2(fast)]
pub fn op_is_mouse_button_pressed(state: &mut OpState, button: u8) -> bool {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow().mouse_buttons_pressed.contains(&button)
}
#[deno_core::op2(fast)]
pub fn op_get_delta_time(state: &mut OpState) -> f64 {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow().delta_time
}
#[deno_core::op2(fast)]
pub fn op_create_solid_texture(
state: &mut OpState,
#[string] name: &str,
r: u32,
g: u32,
b: u32,
a: u32,
) -> u32 {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
let mut br = bridge.borrow_mut();
let key = format!("__solid__{name}");
if let Some(&id) = br.texture_path_to_id.get(&key) {
return id;
}
let id = br.next_texture_id;
br.next_texture_id += 1;
br.texture_path_to_id.insert(key.clone(), id);
br.texture_load_queue
.push((format!("__solid__:{name}:{r}:{g}:{b}:{a}"), id));
id
}
#[deno_core::op2(fast)]
pub fn op_upload_rgba_texture(
state: &mut OpState,
#[string] name: &str,
width: f64,
height: f64,
#[buffer] pixels: &[u8],
) -> u32 {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
let mut b = bridge.borrow_mut();
let key = format!("__raw__:{name}");
if let Some(&id) = b.texture_path_to_id.get(&key) {
return id;
}
let id = b.next_texture_id;
b.next_texture_id += 1;
b.texture_path_to_id.insert(key, id);
b.raw_texture_upload_queue.push((
id,
width as u32,
height as u32,
pixels.to_vec(),
));
id
}
#[deno_core::op2(fast)]
pub fn op_create_tilemap(
state: &mut OpState,
texture_id: u32,
width: u32,
height: u32,
tile_size: f64,
atlas_columns: u32,
atlas_rows: u32,
) -> u32 {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge
.borrow_mut()
.tilemaps
.create(texture_id, width, height, tile_size as f32, atlas_columns, atlas_rows)
}
#[deno_core::op2(fast)]
pub fn op_set_tile(state: &mut OpState, tilemap_id: u32, gx: u32, gy: u32, tile_id: u32) {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
if let Some(tm) = bridge.borrow_mut().tilemaps.get_mut(tilemap_id) {
tm.set_tile(gx, gy, tile_id as u16);
}
}
#[deno_core::op2(fast)]
pub fn op_get_tile(state: &mut OpState, tilemap_id: u32, gx: u32, gy: u32) -> u32 {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge
.borrow()
.tilemaps
.get(tilemap_id)
.map(|tm| tm.get_tile(gx, gy) as u32)
.unwrap_or(0)
}
#[deno_core::op2(fast)]
pub fn op_draw_tilemap(state: &mut OpState, tilemap_id: u32, world_x: f64, world_y: f64, layer: i32) {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
let mut b = bridge.borrow_mut();
let cam_x = b.camera_x;
let cam_y = b.camera_y;
let cam_zoom = b.camera_zoom;
let vp_w = 800.0;
let vp_h = 600.0;
if let Some(tm) = b.tilemaps.get(tilemap_id) {
let cmds = tm.bake_visible(world_x as f32, world_y as f32, layer, cam_x, cam_y, cam_zoom, vp_w, vp_h);
b.sprite_commands.extend(cmds);
}
}
#[deno_core::op2(fast)]
pub fn op_set_ambient_light(state: &mut OpState, r: f64, g: f64, b: f64) {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow_mut().ambient_light = [r as f32, g as f32, b as f32];
}
#[deno_core::op2(fast)]
pub fn op_add_point_light(
state: &mut OpState,
x: f64,
y: f64,
radius: f64,
r: f64,
g: f64,
b: f64,
intensity: f64,
) {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow_mut().point_lights.push(PointLight {
x: x as f32,
y: y as f32,
radius: radius as f32,
r: r as f32,
g: g as f32,
b: b as f32,
intensity: intensity as f32,
});
}
#[deno_core::op2(fast)]
pub fn op_clear_lights(state: &mut OpState) {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow_mut().point_lights.clear();
}
#[deno_core::op2(fast)]
pub fn op_load_sound(state: &mut OpState, #[string] path: &str) -> u32 {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
let mut b = bridge.borrow_mut();
let resolved = if std::path::Path::new(path).is_absolute() {
path.to_string()
} else {
b.base_dir.join(path).to_string_lossy().to_string()
};
if let Some(&id) = b.sound_path_to_id.get(&resolved) {
return id;
}
let id = b.next_sound_id;
b.next_sound_id += 1;
b.sound_path_to_id.insert(resolved.clone(), id);
b.audio_commands.push(BridgeAudioCommand::LoadSound { id, path: resolved });
id
}
#[deno_core::op2(fast)]
pub fn op_stop_all_sounds(state: &mut OpState) {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow_mut().audio_commands.push(BridgeAudioCommand::StopAll);
}
#[deno_core::op2(fast)]
pub fn op_set_master_volume(state: &mut OpState, volume: f64) {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow_mut().audio_commands.push(BridgeAudioCommand::SetMasterVolume { volume: volume as f32 });
}
#[deno_core::op2(fast)]
pub fn op_create_font_texture(state: &mut OpState) -> u32 {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
let mut b = bridge.borrow_mut();
let key = "__builtin_font__".to_string();
if let Some(&id) = b.texture_path_to_id.get(&key) {
return id;
}
let id = b.next_texture_id;
b.next_texture_id += 1;
b.texture_path_to_id.insert(key, id);
b.font_texture_queue.push(id);
id
}
#[deno_core::op2]
#[serde]
pub fn op_get_viewport_size(state: &mut OpState) -> Vec<f64> {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
let b = bridge.borrow();
vec![b.viewport_width as f64, b.viewport_height as f64]
}
#[deno_core::op2(fast)]
pub fn op_get_scale_factor(state: &mut OpState) -> f64 {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow().scale_factor as f64
}
#[deno_core::op2(fast)]
pub fn op_set_background_color(state: &mut OpState, r: f64, g: f64, b: f64) {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
let mut br = bridge.borrow_mut();
br.clear_color = [r as f32, g as f32, b as f32, 1.0];
}
#[deno_core::op2(fast)]
pub fn op_save_file(state: &mut OpState, #[string] key: &str, #[string] value: &str) -> bool {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
let save_dir = bridge.borrow().save_dir.clone();
if !key.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-') {
return false;
}
if std::fs::create_dir_all(&save_dir).is_err() {
return false;
}
let path = save_dir.join(format!("{key}.json"));
std::fs::write(path, value).is_ok()
}
#[deno_core::op2]
#[string]
pub fn op_load_file(state: &mut OpState, #[string] key: &str) -> String {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
let save_dir = bridge.borrow().save_dir.clone();
let path = save_dir.join(format!("{key}.json"));
std::fs::read_to_string(path).unwrap_or_default()
}
#[deno_core::op2(fast)]
pub fn op_delete_file(state: &mut OpState, #[string] key: &str) -> bool {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
let save_dir = bridge.borrow().save_dir.clone();
let path = save_dir.join(format!("{key}.json"));
std::fs::remove_file(path).is_ok()
}
#[deno_core::op2]
#[serde]
pub fn op_list_save_files(state: &mut OpState) -> Vec<String> {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
let save_dir = bridge.borrow().save_dir.clone();
let mut keys = Vec::new();
if let Ok(entries) = std::fs::read_dir(&save_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().map_or(false, |ext| ext == "json") {
if let Some(stem) = path.file_stem() {
keys.push(stem.to_string_lossy().to_string());
}
}
}
}
keys.sort();
keys
}
#[deno_core::op2(fast)]
pub fn op_create_shader(state: &mut OpState, #[string] name: &str, #[string] source: &str) -> u32 {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
let mut b = bridge.borrow_mut();
let id = b.next_shader_id;
b.next_shader_id += 1;
b.shader_create_queue
.push((id, name.to_string(), source.to_string()));
id
}
#[deno_core::op2(fast)]
pub fn op_set_shader_param(
state: &mut OpState,
shader_id: u32,
index: u32,
x: f64,
y: f64,
z: f64,
w: f64,
) {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow_mut().shader_param_queue.push((
shader_id,
index,
[x as f32, y as f32, z as f32, w as f32],
));
}
#[deno_core::op2(fast)]
pub fn op_add_effect(state: &mut OpState, #[string] effect_type: &str) -> u32 {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
let mut b = bridge.borrow_mut();
let id = b.next_effect_id;
b.next_effect_id += 1;
b.effect_create_queue
.push((id, effect_type.to_string()));
id
}
#[deno_core::op2(fast)]
pub fn op_set_effect_param(
state: &mut OpState,
effect_id: u32,
index: u32,
x: f64,
y: f64,
z: f64,
w: f64,
) {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow_mut().effect_param_queue.push((
effect_id,
index,
[x as f32, y as f32, z as f32, w as f32],
));
}
#[deno_core::op2(fast)]
pub fn op_remove_effect(state: &mut OpState, effect_id: u32) {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow_mut().effect_remove_queue.push(effect_id);
}
#[deno_core::op2(fast)]
pub fn op_clear_effects(state: &mut OpState) {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow_mut().effect_clear = true;
}
#[deno_core::op2(fast)]
pub fn op_set_camera_bounds(state: &mut OpState, min_x: f64, min_y: f64, max_x: f64, max_y: f64) {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow_mut().camera_bounds = Some(CameraBounds {
min_x: min_x as f32,
min_y: min_y as f32,
max_x: max_x as f32,
max_y: max_y as f32,
});
}
#[deno_core::op2(fast)]
pub fn op_clear_camera_bounds(state: &mut OpState) {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow_mut().camera_bounds = None;
}
#[deno_core::op2]
#[serde]
pub fn op_get_camera_bounds(state: &mut OpState) -> Vec<f64> {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
let b = bridge.borrow();
match b.camera_bounds {
Some(bounds) => vec![
bounds.min_x as f64,
bounds.min_y as f64,
bounds.max_x as f64,
bounds.max_y as f64,
],
None => vec![],
}
}
#[deno_core::op2(fast)]
pub fn op_enable_gi(state: &mut OpState) {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow_mut().gi_enabled = true;
}
#[deno_core::op2(fast)]
pub fn op_disable_gi(state: &mut OpState) {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow_mut().gi_enabled = false;
}
#[deno_core::op2(fast)]
pub fn op_set_gi_intensity(state: &mut OpState, intensity: f64) {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow_mut().gi_intensity = intensity as f32;
}
#[deno_core::op2(fast)]
pub fn op_set_gi_quality(state: &mut OpState, probe_spacing: f64, interval: f64, cascade_count: f64) {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
let mut b = bridge.borrow_mut();
if probe_spacing > 0.0 {
b.gi_probe_spacing = Some(probe_spacing as f32);
}
if interval > 0.0 {
b.gi_interval = Some(interval as f32);
}
if cascade_count > 0.0 {
b.gi_cascade_count = Some(cascade_count as u32);
}
}
#[deno_core::op2(fast)]
pub fn op_add_emissive(
state: &mut OpState,
x: f64,
y: f64,
w: f64,
h: f64,
r: f64,
g: f64,
b: f64,
intensity: f64,
) {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow_mut().emissives.push([
x as f32,
y as f32,
w as f32,
h as f32,
r as f32,
g as f32,
b as f32,
intensity as f32,
]);
}
#[deno_core::op2(fast)]
pub fn op_clear_emissives(state: &mut OpState) {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow_mut().emissives.clear();
}
#[deno_core::op2(fast)]
pub fn op_add_occluder(state: &mut OpState, x: f64, y: f64, w: f64, h: f64) {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow_mut().occluders.push([x as f32, y as f32, w as f32, h as f32]);
}
#[deno_core::op2(fast)]
pub fn op_clear_occluders(state: &mut OpState) {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow_mut().occluders.clear();
}
#[deno_core::op2(fast)]
pub fn op_add_directional_light(
state: &mut OpState,
angle: f64,
r: f64,
g: f64,
b: f64,
intensity: f64,
) {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow_mut().directional_lights.push([
angle as f32,
r as f32,
g as f32,
b as f32,
intensity as f32,
]);
}
#[deno_core::op2(fast)]
pub fn op_add_spot_light(
state: &mut OpState,
x: f64,
y: f64,
angle: f64,
spread: f64,
range: f64,
r: f64,
g: f64,
b: f64,
intensity: f64,
) {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow_mut().spot_lights.push([
x as f32,
y as f32,
angle as f32,
spread as f32,
range as f32,
r as f32,
g as f32,
b as f32,
intensity as f32,
]);
}
#[deno_core::op2(fast)]
pub fn op_play_sound_ex(
state: &mut OpState,
sound_id: u32,
instance_id: f64,
volume: f64,
looping: bool,
bus: u32,
pan: f64,
pitch: f64,
low_pass_freq: u32,
reverb_mix: f64,
reverb_delay_ms: u32,
) {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow_mut().audio_commands.push(BridgeAudioCommand::PlaySoundEx {
sound_id,
instance_id: instance_id as u64,
volume: volume as f32,
looping,
bus,
pan: pan as f32,
pitch: pitch as f32,
low_pass_freq,
reverb_mix: reverb_mix as f32,
reverb_delay_ms,
});
}
#[deno_core::op2(fast)]
pub fn op_play_sound_spatial(
state: &mut OpState,
sound_id: u32,
instance_id: f64,
volume: f64,
looping: bool,
bus: u32,
pitch: f64,
source_x: f64,
source_y: f64,
listener_x: f64,
listener_y: f64,
) {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow_mut().audio_commands.push(BridgeAudioCommand::PlaySoundSpatial {
sound_id,
instance_id: instance_id as u64,
volume: volume as f32,
looping,
bus,
pitch: pitch as f32,
source_x: source_x as f32,
source_y: source_y as f32,
listener_x: listener_x as f32,
listener_y: listener_y as f32,
});
}
#[deno_core::op2(fast)]
pub fn op_stop_instance(state: &mut OpState, instance_id: f64) {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow_mut().audio_commands.push(BridgeAudioCommand::StopInstance {
instance_id: instance_id as u64,
});
}
#[deno_core::op2(fast)]
pub fn op_set_instance_volume(state: &mut OpState, instance_id: f64, volume: f64) {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow_mut().audio_commands.push(BridgeAudioCommand::SetInstanceVolume {
instance_id: instance_id as u64,
volume: volume as f32,
});
}
#[deno_core::op2(fast)]
pub fn op_set_instance_pitch(state: &mut OpState, instance_id: f64, pitch: f64) {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow_mut().audio_commands.push(BridgeAudioCommand::SetInstancePitch {
instance_id: instance_id as u64,
pitch: pitch as f32,
});
}
#[deno_core::op2(fast)]
pub fn op_update_spatial_positions(state: &mut OpState, #[string] data_json: &str, listener_x: f64, listener_y: f64) {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
let mut updates = Vec::new();
if let Some(ids_start) = data_json.find("\"instanceIds\":[") {
if let Some(xs_start) = data_json.find("\"sourceXs\":[") {
if let Some(ys_start) = data_json.find("\"sourceYs\":[") {
let ids_str = &data_json[ids_start + 15..];
let xs_str = &data_json[xs_start + 12..];
let ys_str = &data_json[ys_start + 12..];
let ids_end = ids_str.find(']').unwrap_or(0);
let xs_end = xs_str.find(']').unwrap_or(0);
let ys_end = ys_str.find(']').unwrap_or(0);
let ids: Vec<u64> = ids_str[..ids_end]
.split(',')
.filter_map(|s| s.trim().parse().ok())
.collect();
let xs: Vec<f32> = xs_str[..xs_end]
.split(',')
.filter_map(|s| s.trim().parse().ok())
.collect();
let ys: Vec<f32> = ys_str[..ys_end]
.split(',')
.filter_map(|s| s.trim().parse().ok())
.collect();
for i in 0..ids.len().min(xs.len()).min(ys.len()) {
updates.push((ids[i], xs[i], ys[i]));
}
}
}
}
bridge.borrow_mut().audio_commands.push(BridgeAudioCommand::UpdateSpatialPositions {
updates,
listener_x: listener_x as f32,
listener_y: listener_y as f32,
});
}
#[deno_core::op2(fast)]
pub fn op_set_bus_volume(state: &mut OpState, bus: u32, volume: f64) {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow_mut().audio_commands.push(BridgeAudioCommand::SetBusVolume {
bus,
volume: volume as f32,
});
}
#[deno_core::op2]
#[string]
pub fn op_create_msdf_builtin_font(state: &mut OpState) -> String {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
let mut b = bridge.borrow_mut();
let key = "__msdf_builtin__".to_string();
if let Some(&tex_id) = b.texture_path_to_id.get(&key) {
let font_id_key = format!("__msdf_font_{tex_id}__");
if let Some(&font_id) = b.texture_path_to_id.get(&font_id_key) {
let pool = &b.msdf_shader_pool;
let shader_id = pool.first().copied().unwrap_or(0);
let pool_json: Vec<String> = pool.iter().map(|id| id.to_string()).collect();
return format!(
"{{\"fontId\":{},\"textureId\":{},\"shaderId\":{},\"shaderPool\":[{}]}}",
font_id, tex_id, shader_id, pool_json.join(",")
);
}
}
let tex_id = b.next_texture_id;
b.next_texture_id += 1;
b.texture_path_to_id.insert(key, tex_id);
let (_pixels, _width, _height, mut font) =
crate::renderer::msdf::generate_builtin_msdf_font();
font.texture_id = tex_id;
let font_id = b.msdf_fonts.register(font);
b.texture_path_to_id
.insert(format!("__msdf_font_{tex_id}__"), font_id);
b.msdf_builtin_queue.push((font_id, tex_id));
let pool = ensure_msdf_shader_pool(&mut b);
let shader_id = pool.first().copied().unwrap_or(0);
let pool_json: Vec<String> = pool.iter().map(|id| id.to_string()).collect();
format!(
"{{\"fontId\":{},\"textureId\":{},\"shaderId\":{},\"shaderPool\":[{}]}}",
font_id, tex_id, shader_id, pool_json.join(",")
)
}
#[deno_core::op2]
#[string]
pub fn op_get_msdf_glyphs(
state: &mut OpState,
font_id: u32,
#[string] text: &str,
) -> String {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
let b = bridge.borrow();
let font = match b.msdf_fonts.get(font_id) {
Some(f) => f,
None => return "[]".to_string(),
};
let mut entries = Vec::new();
for ch in text.chars() {
if let Some(glyph) = font.get_glyph(ch) {
entries.push(format!(
"{{\"char\":{},\"uv\":[{},{},{},{}],\"advance\":{},\"width\":{},\"height\":{},\"offsetX\":{},\"offsetY\":{}}}",
ch as u32,
glyph.uv_x, glyph.uv_y, glyph.uv_w, glyph.uv_h,
glyph.advance, glyph.width, glyph.height,
glyph.offset_x, glyph.offset_y,
));
}
}
format!("[{}]", entries.join(","))
}
#[deno_core::op2]
#[string]
pub fn op_get_msdf_font_info(state: &mut OpState, font_id: u32) -> String {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
let b = bridge.borrow();
match b.msdf_fonts.get(font_id) {
Some(font) => format!(
"{{\"fontSize\":{},\"lineHeight\":{},\"distanceRange\":{},\"textureId\":{}}}",
font.font_size, font.line_height, font.distance_range, font.texture_id,
),
None => "null".to_string(),
}
}
#[deno_core::op2]
#[string]
pub fn op_load_msdf_font(
state: &mut OpState,
#[string] atlas_path: &str,
#[string] metrics_json_or_path: &str,
) -> String {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
let mut b = bridge.borrow_mut();
let resolved = if std::path::Path::new(atlas_path).is_absolute() {
atlas_path.to_string()
} else {
b.base_dir.join(atlas_path).to_string_lossy().to_string()
};
let tex_id = if let Some(&id) = b.texture_path_to_id.get(&resolved) {
id
} else {
let id = b.next_texture_id;
b.next_texture_id += 1;
b.texture_path_to_id.insert(resolved.clone(), id);
b.msdf_texture_load_queue.push((resolved, id));
id
};
let metrics_json: String = if metrics_json_or_path.trim_start().starts_with('{') {
metrics_json_or_path.to_string()
} else {
let json_path = if std::path::Path::new(metrics_json_or_path).is_absolute() {
metrics_json_or_path.to_string()
} else {
b.base_dir
.join(metrics_json_or_path)
.to_string_lossy()
.to_string()
};
match std::fs::read_to_string(&json_path) {
Ok(content) => content,
Err(e) => {
return format!("{{\"error\":\"Failed to read metrics file {}: {}\"}}", json_path, e);
}
}
};
let font = match crate::renderer::msdf::parse_msdf_metrics(&metrics_json, tex_id) {
Ok(f) => f,
Err(e) => {
return format!("{{\"error\":\"{}\"}}", e);
}
};
let font_id = b.msdf_fonts.register(font);
let pool = ensure_msdf_shader_pool(&mut b);
let shader_id = pool.first().copied().unwrap_or(0);
let pool_json: Vec<String> = pool.iter().map(|id| id.to_string()).collect();
format!(
"{{\"fontId\":{},\"textureId\":{},\"shaderId\":{},\"shaderPool\":[{}]}}",
font_id, tex_id, shader_id, pool_json.join(",")
)
}
const MSDF_SHADER_POOL_SIZE: usize = 8;
fn ensure_msdf_shader_pool(b: &mut RenderBridgeState) -> Vec<u32> {
if !b.msdf_shader_pool.is_empty() {
return b.msdf_shader_pool.clone();
}
let source = crate::renderer::msdf::MSDF_FRAGMENT_SOURCE.to_string();
let mut pool = Vec::with_capacity(MSDF_SHADER_POOL_SIZE);
for _ in 0..MSDF_SHADER_POOL_SIZE {
let id = b.next_shader_id;
b.next_shader_id += 1;
b.msdf_shader_queue.push((id, source.clone()));
pool.push(id);
}
b.msdf_shader_pool = pool.clone();
pool
}
#[deno_core::op2(fast)]
pub fn op_get_gamepad_count(state: &mut OpState) -> u32 {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow().gamepad_count
}
#[deno_core::op2]
#[string]
pub fn op_get_gamepad_name(state: &mut OpState) -> String {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow().gamepad_name.clone()
}
#[deno_core::op2(fast)]
pub fn op_is_gamepad_button_down(state: &mut OpState, #[string] button: &str) -> bool {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow().gamepad_buttons_down.contains(button)
}
#[deno_core::op2(fast)]
pub fn op_is_gamepad_button_pressed(state: &mut OpState, #[string] button: &str) -> bool {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow().gamepad_buttons_pressed.contains(button)
}
#[deno_core::op2(fast)]
pub fn op_get_gamepad_axis(state: &mut OpState, #[string] axis: &str) -> f64 {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow().gamepad_axes.get(axis).copied().unwrap_or(0.0) as f64
}
#[deno_core::op2(fast)]
pub fn op_get_touch_count(state: &mut OpState) -> u32 {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow().touch_count
}
#[deno_core::op2]
#[serde]
pub fn op_get_touch_position(state: &mut OpState, index: u32) -> Vec<f64> {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
let b = bridge.borrow();
if let Some(&(_, x, y)) = b.touch_points.get(index as usize) {
vec![x as f64, y as f64]
} else {
vec![]
}
}
#[deno_core::op2(fast)]
pub fn op_is_touch_active(state: &mut OpState) -> bool {
let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
bridge.borrow().touch_count > 0
}
deno_core::extension!(
render_ext,
ops = [
op_clear_sprites,
op_submit_sprite_batch,
op_set_camera,
op_get_camera,
op_load_texture,
op_load_texture_linear,
op_upload_rgba_texture,
op_is_key_down,
op_is_key_pressed,
op_get_mouse_position,
op_is_mouse_button_down,
op_is_mouse_button_pressed,
op_get_delta_time,
op_create_solid_texture,
op_create_tilemap,
op_set_tile,
op_get_tile,
op_draw_tilemap,
op_set_ambient_light,
op_add_point_light,
op_clear_lights,
op_load_sound,
op_stop_all_sounds,
op_set_master_volume,
op_play_sound_ex,
op_play_sound_spatial,
op_stop_instance,
op_set_instance_volume,
op_set_instance_pitch,
op_update_spatial_positions,
op_set_bus_volume,
op_create_font_texture,
op_get_viewport_size,
op_get_scale_factor,
op_set_background_color,
op_save_file,
op_load_file,
op_delete_file,
op_list_save_files,
op_create_shader,
op_set_shader_param,
op_add_effect,
op_set_effect_param,
op_remove_effect,
op_clear_effects,
op_set_camera_bounds,
op_clear_camera_bounds,
op_get_camera_bounds,
op_enable_gi,
op_disable_gi,
op_set_gi_intensity,
op_set_gi_quality,
op_add_emissive,
op_clear_emissives,
op_add_occluder,
op_clear_occluders,
op_add_directional_light,
op_add_spot_light,
op_create_msdf_builtin_font,
op_get_msdf_glyphs,
op_get_msdf_font_info,
op_load_msdf_font,
op_get_gamepad_count,
op_get_gamepad_name,
op_is_gamepad_button_down,
op_is_gamepad_button_pressed,
op_get_gamepad_axis,
op_get_touch_count,
op_get_touch_position,
op_is_touch_active,
],
);