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_draw_sprite` and `op_submit_sprite_batch` route 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);