1 //------------------------------------------------------------------------------
2 //  instancingcompute.d
3 //
4 //  Like instancing.d, but use a compute shader to update particles.
5 //------------------------------------------------------------------------------
6 module examples.instancingcompute;
7 
8 private:
9 
10 import sg = sokol.gfx;
11 import sapp = sokol.app;
12 import slog = sokol.log;
13 import handmade.math: Mat4, Vec3;
14 import sglue = sokol.glue;
15 import shd = shaders.instancingcompute;
16 
17 extern (C):
18 @safe nothrow @nogc:
19 
20 enum max_particles = 512 * 1024;
21 enum num_particles_emitted_per_frame = 10;
22 
23 struct ComputeState {
24     sg.Pipeline pip = {};
25     sg.Bindings bind = {};
26 }
27 
28 struct DisplayState {
29     sg.Pipeline pip = {};
30     sg.Bindings bind = {};
31     sg.PassAction pass_action = {
32         colors: [
33             {
34                 load_action: sg.LoadAction.Clear,
35                 clear_value: {r: 0.0, g: 0.1, b: 0.2, a: 1 },
36             }
37         ]
38     };
39 }
40 
41 struct State {
42     int num_particles = 0;
43     float ry = 0;
44     ComputeState compute;
45     DisplayState display;
46 
47     Mat4 view() @nogc nothrow const {
48         return Mat4.lookAt(Vec3(0.0, 1.5, 12.0), Vec3.zero(), Vec3.up());
49     }
50 }
51 
52 static State state;
53 
54 void init() {
55     sg.Desc gfxd = {
56         environment: sglue.environment,
57         logger: { func: &slog.func },
58     };
59     sg.setup(gfxd);
60 
61     // if compute shaders not supported, clear to red color and early out
62     if (!sg.queryFeatures().compute) {
63         state.display.pass_action.colors[0].clear_value.r = 1.0;
64         state.display.pass_action.colors[0].clear_value.g = 0.0;
65         state.display.pass_action.colors[0].clear_value.b = 0.0;
66         return;
67     }
68 
69     // a zero-initialized storage buffer for the particle state
70     sg.BufferDesc sbufd = {
71         usage: { storage_buffer: true },
72         size: shd.Particle.sizeof * max_particles,
73         label: "particle-buffer",
74     };
75     sg.Buffer sbuf = sg.makeBuffer(sbufd);
76     sg.ViewDesc sbufviewd = {
77         storage_buffer: { buffer: sbuf },
78     };
79     sg.View sbuf_view = sg.makeView(sbufviewd);
80     state.compute.bind.views[shd.VIEW_CS_SSBO] = sbuf_view;
81     state.display.bind.views[shd.VIEW_VS_SSBO] = sbuf_view;
82 
83     // a compute shader and pipeline object for updating the particle state
84     sg.PipelineDesc upipd = {
85         compute: true,
86         shader: sg.makeShader(shd.updateShaderDesc(sg.queryBackend())),
87         label: "update-pipeline",
88     };
89     state.compute.pip = sg.makePipeline(upipd);
90 
91     // vertex and index buffer for the particle geometry
92     immutable float r = 0.05;
93     float[42] vertices = [
94         0.0, -r, 0.0, 1.0, 0.0, 0.0, 1.0,
95         r, 0.0, r, 0.0, 1.0, 0.0, 1.0,
96         r, 0.0, -r, 0.0, 0.0, 1.0, 1.0,
97         -r, 0.0, -r, 1.0, 1.0, 0.0, 1.0,
98         -r, 0.0, r, 0.0, 1.0, 1.0, 1.0,
99         0.0, r, 0.0, 1.0, 0.0, 1.0, 1.0,
100     ];
101     ushort[24] indices = [
102         2, 1, 0, 3, 2, 0,
103         4, 3, 0, 1, 4, 0,
104         5, 1, 2, 5, 2, 3,
105         5, 3, 4, 5, 4, 1,
106     ];
107     sg.BufferDesc vbufd = {
108         data: { ptr: vertices.ptr, size: vertices.sizeof },
109         label: "geometry-vbuf",
110     };
111     sg.BufferDesc ibufd = {
112         usage: { index_buffer: true },
113         data: { ptr: indices.ptr, size: indices.sizeof },
114         label: "geometry-ibuf",
115     };
116     state.display.bind.vertex_buffers[0] = sg.makeBuffer(vbufd);
117     state.display.bind.index_buffer = sg.makeBuffer(ibufd);
118 
119     // shader and pipeline for rendering the particles, this uses
120     // the compute-updated storage buffer to provide the particle positions
121     sg.PipelineDesc rpipd = {
122         shader: sg.makeShader(shd.displayShaderDesc(sg.queryBackend())),
123         layout: {
124             attrs: [
125                 shd.ATTR_DISPLAY_POS: { format: sg.VertexFormat.Float3 },
126                 shd.ATTR_DISPLAY_COLOR0: { format: sg.VertexFormat.Float4 },
127             ],
128         },
129         index_type: sg.IndexType.Uint16,
130         cull_mode: sg.CullMode.Back,
131         depth: {
132             compare: sg.CompareFunc.Less_equal,
133             write_enabled: true,
134         },
135         label: "render-pipeline",
136     };
137     state.display.pip = sg.makePipeline(rpipd);
138 
139     // one-time init of particle velocities via a compute shader
140     sg.PipelineDesc ipipd = {
141         compute: true,
142         shader: sg.makeShader(shd.initShaderDesc(sg.queryBackend())),
143     };
144     sg.Pipeline ipip = sg.makePipeline(ipipd);
145     sg.Pass pass = { compute: true };
146     sg.beginPass(pass);
147     sg.applyPipeline(ipip);
148     sg.applyBindings(state.compute.bind);
149     sg.dispatch(max_particles / 64, 1, 1);
150     sg.endPass();
151 }
152 
153 void frame() {
154     if (!sg.queryFeatures().compute) {
155         drawFallback();
156         return;
157     }
158 
159     state.num_particles += num_particles_emitted_per_frame;
160     if (state.num_particles > max_particles) {
161         state.num_particles = max_particles;
162     }
163     float dt = sapp.frameDuration();
164 
165     // compute pass to update particle positions
166     shd.CsParams cs_params = {
167         dt: dt,
168         num_particles: state.num_particles,
169     };
170     sg.Range cs_params_range = { ptr: &cs_params, size: cs_params.sizeof };
171     sg.Pass cpass = { compute: true, label: "compute-pass" };
172     sg.beginPass(cpass);
173     sg.applyPipeline(state.compute.pip);
174     sg.applyBindings(state.compute.bind);
175     sg.applyUniforms(shd.UB_CS_PARAMS, cs_params_range);
176     sg.dispatch((state.num_particles + 63) / 64, 1, 1);
177     sg.endPass();
178 
179     // render pass to render the particles via instancing, with the
180     // instance positions coming from the storage buffer
181     state.ry += 60.0 * dt;
182     shd.VsParams vs_params = computeVsParams(1.0, state.ry);
183     sg.Range vs_params_range = { ptr: &vs_params, size: vs_params.sizeof };
184     sg.Pass rpass = {
185         action: state.display.pass_action,
186         swapchain: sglue.swapchain(),
187         label: "render-pass",
188     };
189     sg.beginPass(rpass);
190     sg.applyPipeline(state.display.pip);
191     sg.applyBindings(state.display.bind);
192     sg.applyUniforms(shd.UB_VS_PARAMS, vs_params_range);
193     sg.draw(0, 24, state.num_particles);
194     sg.endPass();
195     sg.commit();
196 }
197 
198 void cleanup() {
199     sg.shutdown();
200 }
201 
202 void drawFallback() {
203     sg.Pass rpass = {
204         action: state.display.pass_action,
205         swapchain: sglue.swapchain(),
206         label: "render-pass",
207     };
208     sg.beginPass(rpass);
209     sg.endPass();
210     sg.commit();
211 }
212 
213 shd.VsParams computeVsParams(float rx, float ry) @nogc nothrow
214 {
215     immutable proj = Mat4.perspective(60.0, sapp.widthf() / sapp.heightf(), 0.01, 50.0);
216     immutable rxm = Mat4.rotate(rx, Vec3(1.0, 0.0, 0.0));
217     immutable rym = Mat4.rotate(ry, Vec3(0.0, 1.0, 0.0));
218     immutable model = Mat4.mul(rxm, rym);
219     immutable view_proj = Mat4.mul(proj, state.view());
220     shd.VsParams mvp = { mvp: Mat4.mul(view_proj, model) };
221     return mvp;
222 }
223 
224 void main() {
225     sapp.Desc adesc = {
226         init_cb: &init,
227         frame_cb: &frame,
228         cleanup_cb: &cleanup,
229         width: 800,
230         height: 600,
231         sample_count: 4,
232         window_title: "instancingcompute.d",
233         icon: { sokol_default: true },
234         logger: { func: &slog.func },
235     };
236     sapp.run(adesc);
237 }
238 
239 version (WebAssembly)
240 {
241     debug
242     {
243         import emscripten.assertd;
244     }
245 }