Skip to main content

celestialsim/gpu/
device.rs

1//! Pipeline / buffer / texture creation helpers (render-thread only).
2//!
3//! Every creator here hands its RID straight to an [`Owned`] handle bound to the caller's
4//! [`RidSink`], so the resource is released by `Drop` alone — see `gpu::owned` for the
5//! drop-order contract. Nothing in this module frees a RID explicitly.
6
7use std::sync::Arc;
8
9use godot::classes::rendering_device::{
10    DataFormat, ShaderLanguage, ShaderStage, TextureUsageBits, UniformType,
11};
12use godot::classes::{
13    RdShaderSource, RdShaderSpirv, RdTextureFormat, RdTextureView, RdUniform, RenderingDevice,
14};
15use godot::prelude::*;
16
17use super::owned::{Owned, RdBuffer, RdPipeline, RdShader, RdTexture, RidSink};
18
19/// Compute shader + pipeline from SPIR-V bytes produced by `build.rs`.
20///
21/// On a pipeline-create failure the early return drops the `RdShader`, which frees the
22/// shader through `sink` — no manual cleanup.
23pub fn compute_pipeline(
24    rd: &mut Gd<RenderingDevice>,
25    sink: &Arc<dyn RidSink>,
26    spirv: &[u8],
27    name: &str,
28) -> Option<(RdShader, RdPipeline)> {
29    if spirv.is_empty() {
30        godot_error!("[celestial] {name}: empty SPIR-V — was slangc installed at build time?");
31        return None;
32    }
33    let mut sp = RdShaderSpirv::new_gd();
34    sp.set_stage_bytecode(ShaderStage::COMPUTE, &PackedByteArray::from(spirv));
35    let shader_rid = rd.shader_create_from_spirv(&sp);
36    if !shader_rid.is_valid() {
37        godot_error!("[celestial] {name}: shader_create_from_spirv failed");
38        return None;
39    }
40    let shader: RdShader = Owned::new(shader_rid, sink.clone());
41    let pipeline_rid = rd.compute_pipeline_create(shader.rid());
42    if !pipeline_rid.is_valid() {
43        godot_error!("[celestial] {name}: compute_pipeline_create failed");
44        return None; // dropping `shader` frees it
45    }
46    Some((shader, Owned::new(pipeline_rid, sink.clone())))
47}
48
49/// Compute shader + pipeline from GLSL **source compiled at runtime** by Godot's
50/// own shader compiler (no `slangc`, nothing extra shipped). Used for the
51/// user-authored custom-surface terrain (`crate::custom_surface`): a compile
52/// error is logged with Godot's message and returns `None` so the caller can
53/// fall back to the built-in path instead of crashing.
54pub fn compute_pipeline_from_glsl(
55    rd: &mut Gd<RenderingDevice>,
56    sink: &Arc<dyn RidSink>,
57    glsl: &str,
58    name: &str,
59) -> Option<(RdShader, RdPipeline)> {
60    let mut src = RdShaderSource::new_gd();
61    src.set_language(ShaderLanguage::GLSL);
62    src.set_stage_source(ShaderStage::COMPUTE, glsl);
63    let Some(spirv) = rd.shader_compile_spirv_from_source_ex(&src).allow_cache(true).done() else {
64        godot_error!("[celestial] {name}: shader_compile_spirv_from_source returned null");
65        return None;
66    };
67    let err = spirv.get_stage_compile_error(ShaderStage::COMPUTE);
68    if !err.is_empty() {
69        godot_error!("[celestial] {name}: GLSL compile error:\n{err}");
70        return None;
71    }
72    let shader_rid = rd.shader_create_from_spirv(&spirv);
73    if !shader_rid.is_valid() {
74        godot_error!("[celestial] {name}: shader_create_from_spirv failed");
75        return None;
76    }
77    let shader: RdShader = Owned::new(shader_rid, sink.clone());
78    let pipeline_rid = rd.compute_pipeline_create(shader.rid());
79    if !pipeline_rid.is_valid() {
80        godot_error!("[celestial] {name}: compute_pipeline_create failed");
81        return None; // dropping `shader` frees it
82    }
83    Some((shader, Owned::new(pipeline_rid, sink.clone())))
84}
85
86/// Uninitialized storage buffer of `size` bytes.
87pub fn storage_buffer_empty(
88    rd: &mut Gd<RenderingDevice>,
89    sink: &Arc<dyn RidSink>,
90    size: u64,
91) -> RdBuffer {
92    Owned::new(rd.storage_buffer_create_ex(size as u32).done(), sink.clone())
93}
94
95/// Storage buffer pre-filled with `bytes`.
96pub fn storage_buffer(
97    rd: &mut Gd<RenderingDevice>,
98    sink: &Arc<dyn RidSink>,
99    bytes: &[u8],
100) -> RdBuffer {
101    let rid = rd
102        .storage_buffer_create_ex(bytes.len() as u32)
103        .data(&PackedByteArray::from(bytes))
104        .done();
105    Owned::new(rid, sink.clone())
106}
107
108/// RGBA16F storage texture (compute writes, material samples).
109pub fn attribute_texture(
110    rd: &mut Gd<RenderingDevice>,
111    sink: &Arc<dyn RidSink>,
112    width: u32,
113    height: u32,
114) -> RdTexture {
115    let mut fmt = RdTextureFormat::new_gd();
116    fmt.set_format(DataFormat::R16G16B16A16_SFLOAT);
117    fmt.set_width(width);
118    fmt.set_height(height);
119    fmt.set_usage_bits(
120        TextureUsageBits::STORAGE_BIT | TextureUsageBits::SAMPLING_BIT,
121    );
122    Owned::new(rd.texture_create(&fmt, &RdTextureView::new_gd()), sink.clone())
123}
124
125/// RGBA32F storage texture (compute writes world positions, material samples).
126/// One texel per vertex: the surface material reads VERTEX from here because a
127/// spatial shader cannot sample the `verts` storage buffer the readback uses.
128pub fn position_texture(
129    rd: &mut Gd<RenderingDevice>,
130    sink: &Arc<dyn RidSink>,
131    width: u32,
132    height: u32,
133) -> RdTexture {
134    let mut fmt = RdTextureFormat::new_gd();
135    fmt.set_format(DataFormat::R32G32B32A32_SFLOAT);
136    fmt.set_width(width);
137    fmt.set_height(height);
138    // CAN_COPY_FROM so debug tooling can read the drawn positions back.
139    fmt.set_usage_bits(
140        TextureUsageBits::STORAGE_BIT
141            | TextureUsageBits::SAMPLING_BIT
142            | TextureUsageBits::CAN_COPY_FROM_BIT,
143    );
144    Owned::new(rd.texture_create(&fmt, &RdTextureView::new_gd()), sink.clone())
145}
146
147/// RGBA8-UNORM storage texture (compute writes, material samples). Used for the
148/// Phase-4 per-chunk colour/normal detail atlases: `STORAGE_BIT` so the bake
149/// compute pass can write it, `SAMPLING_BIT` so `terrain_chunk.gdshader` can
150/// `texelFetch` it. Normals are encoded `*0.5+0.5` into [0,1] to fit unorm.
151pub fn atlas_texture(
152    rd: &mut Gd<RenderingDevice>,
153    sink: &Arc<dyn RidSink>,
154    width: u32,
155    height: u32,
156) -> RdTexture {
157    let mut fmt = RdTextureFormat::new_gd();
158    fmt.set_format(DataFormat::R8G8B8A8_UNORM);
159    fmt.set_width(width);
160    fmt.set_height(height);
161    // CAN_COPY_FROM so the detail tile can be read back / exported for inspection.
162    fmt.set_usage_bits(
163        TextureUsageBits::STORAGE_BIT
164            | TextureUsageBits::SAMPLING_BIT
165            | TextureUsageBits::CAN_COPY_FROM_BIT,
166    );
167    Owned::new(rd.texture_create(&fmt, &RdTextureView::new_gd()), sink.clone())
168}
169
170pub fn uniform(utype: UniformType, binding: i32, rid: Rid) -> Gd<RdUniform> {
171    let mut u = RdUniform::new_gd();
172    u.set_uniform_type(utype);
173    u.set_binding(binding);
174    u.add_id(rid);
175    u
176}