Skip to main content

arcane_core/scripting/
target_ops.rs

1/// Render-to-texture ops: create off-screen render targets, draw into them,
2/// then use them as TextureIds in subsequent drawSprite calls.
3///
4/// ## API (TS-side)
5/// ```ts
6/// const rt = createRenderTarget(256, 256);  // → RenderTargetId (also usable as TextureId)
7/// beginRenderTarget(rt);
8///   drawSprite(...);  // renders into rt's texture, camera: (0,0) = top-left
9/// endRenderTarget();
10/// drawSprite({ textureId: rt, x: 0, y: 0, w: 256, h: 256 });
11/// ```
12///
13/// ## Design
14/// - `op_create_render_target` allocates an ID from the shared `next_texture_id` counter
15///   (avoiding any collision with regular textures) and queues GPU resource creation.
16/// - `op_begin_render_target` sets `active_target = Some(id)`. While active,
17///   `op_submit_sprite_batch` routes commands to the target's
18///   queue instead of the main bridge sprite list.
19/// - `op_end_render_target` clears `active_target`.
20/// - dev.rs drains `create_queue`, `target_sprite_queues`, and `destroy_queue`
21///   each frame before the main render pass.
22
23use std::cell::RefCell;
24use std::collections::HashMap;
25use std::rc::Rc;
26
27use deno_core::OpState;
28
29use crate::renderer::SpriteCommand;
30use crate::scripting::render_ops::RenderBridgeState;
31
32/// State for all live render targets and the currently active one.
33pub struct TargetState {
34    /// If Some, sprite commands route to this target instead of the main pass.
35    pub active_target: Option<u32>,
36    /// GPU resource creation requests, drained by dev.rs each frame.
37    pub create_queue: Vec<(u32, u32, u32)>, // (id, width, height)
38    /// GPU resource destroy requests, drained by dev.rs each frame.
39    pub destroy_queue: Vec<u32>,
40    /// Per-target sprite command queues, drained by dev.rs for off-screen rendering.
41    pub target_sprite_queues: HashMap<u32, Vec<SpriteCommand>>,
42}
43
44impl TargetState {
45    pub fn new() -> Self {
46        Self {
47            active_target: None,
48            create_queue: Vec::new(),
49            destroy_queue: Vec::new(),
50            target_sprite_queues: HashMap::new(),
51        }
52    }
53}
54
55/// Create an off-screen render target of the given pixel dimensions.
56/// Returns an ID that doubles as both a `RenderTargetId` and a `TextureId`.
57///
58/// The ID is allocated from the shared texture ID pool to guarantee no
59/// collision with regular textures or other render targets.
60#[deno_core::op2(fast)]
61fn op_create_render_target(state: &mut OpState, w: f64, h: f64) -> u32 {
62    // Allocate from shared texture ID counter to avoid any collision
63    let id = {
64        let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
65        let mut b = bridge.borrow_mut();
66        let id = b.next_texture_id;
67        b.next_texture_id += 1;
68        id
69    };
70    let ts = state.borrow_mut::<Rc<RefCell<TargetState>>>();
71    ts.borrow_mut()
72        .create_queue
73        .push((id, w as u32, h as u32));
74    id
75}
76
77/// Route subsequent drawSprite calls into this render target.
78/// Coordinate system inside the target: (0, 0) = top-left of target.
79#[deno_core::op2(fast)]
80fn op_begin_render_target(state: &mut OpState, id: u32) {
81    let ts = state.borrow_mut::<Rc<RefCell<TargetState>>>();
82    ts.borrow_mut().active_target = Some(id);
83}
84
85/// Return to rendering into the main surface.
86#[deno_core::op2(fast)]
87fn op_end_render_target(state: &mut OpState) {
88    let ts = state.borrow_mut::<Rc<RefCell<TargetState>>>();
89    ts.borrow_mut().active_target = None;
90}
91
92/// Free the GPU resources for a render target.
93/// After this call, using the ID as a TextureId produces a transparent sprite.
94#[deno_core::op2(fast)]
95fn op_destroy_render_target(state: &mut OpState, id: u32) {
96    let ts = state.borrow_mut::<Rc<RefCell<TargetState>>>();
97    let mut ts = ts.borrow_mut();
98    ts.destroy_queue.push(id);
99    ts.target_sprite_queues.remove(&id);
100    // If this target was active, end it
101    if ts.active_target == Some(id) {
102        ts.active_target = None;
103    }
104}
105
106deno_core::extension!(
107    target_ext,
108    ops = [
109        op_create_render_target,
110        op_begin_render_target,
111        op_end_render_target,
112        op_destroy_render_target,
113    ],
114);
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn test_target_state_new() {
122        let state = TargetState::new();
123        assert!(state.active_target.is_none());
124        assert!(state.create_queue.is_empty());
125        assert!(state.destroy_queue.is_empty());
126        assert!(state.target_sprite_queues.is_empty());
127    }
128
129    #[test]
130    fn test_set_active_target() {
131        let mut state = TargetState::new();
132        assert!(state.active_target.is_none());
133
134        state.active_target = Some(42);
135        assert_eq!(state.active_target, Some(42));
136
137        state.active_target = None;
138        assert!(state.active_target.is_none());
139    }
140
141    #[test]
142    fn test_create_queue() {
143        let mut state = TargetState::new();
144
145        state.create_queue.push((1, 256, 256));
146        state.create_queue.push((2, 128, 128));
147
148        assert_eq!(state.create_queue.len(), 2);
149        assert_eq!(state.create_queue[0], (1, 256, 256));
150        assert_eq!(state.create_queue[1], (2, 128, 128));
151    }
152
153    #[test]
154    fn test_destroy_queue() {
155        let mut state = TargetState::new();
156
157        state.destroy_queue.push(5);
158        state.destroy_queue.push(10);
159
160        assert_eq!(state.destroy_queue.len(), 2);
161        assert!(state.destroy_queue.contains(&5));
162        assert!(state.destroy_queue.contains(&10));
163    }
164
165    #[test]
166    fn test_target_sprite_queues() {
167        let mut state = TargetState::new();
168
169        state.target_sprite_queues.insert(1, Vec::new());
170        state.target_sprite_queues.insert(2, Vec::new());
171
172        assert!(state.target_sprite_queues.contains_key(&1));
173        assert!(state.target_sprite_queues.contains_key(&2));
174        assert!(!state.target_sprite_queues.contains_key(&3));
175
176        state.target_sprite_queues.remove(&1);
177        assert!(!state.target_sprite_queues.contains_key(&1));
178    }
179
180    #[test]
181    fn test_destroy_clears_active() {
182        let mut state = TargetState::new();
183        state.active_target = Some(5);
184
185        // Simulate destroy logic
186        state.destroy_queue.push(5);
187        state.target_sprite_queues.remove(&5);
188        if state.active_target == Some(5) {
189            state.active_target = None;
190        }
191
192        assert!(state.active_target.is_none());
193    }
194}