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 = examples.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     state.compute.bind.storage_buffers[shd.SBUF_CS_SSBO] = sbuf;
77     state.display.bind.storage_buffers[shd.SBUF_VS_SSBO] = sbuf;
78 
79     // a compute shader and pipeline object for updating the particle state
80     sg.PipelineDesc upipd = {
81         compute: true,
82         shader: sg.makeShader(shd.updateShaderDesc(sg.queryBackend())),
83         label: "update-pipeline",
84     };
85     state.compute.pip = sg.makePipeline(upipd);
86 
87     // vertex and index buffer for the particle geometry
88     immutable float r = 0.05;
89     float[42] vertices = [
90         0.0, -r, 0.0, 1.0, 0.0, 0.0, 1.0,
91         r, 0.0, r, 0.0, 1.0, 0.0, 1.0,
92         r, 0.0, -r, 0.0, 0.0, 1.0, 1.0,
93         -r, 0.0, -r, 1.0, 1.0, 0.0, 1.0,
94         -r, 0.0, r, 0.0, 1.0, 1.0, 1.0,
95         0.0, r, 0.0, 1.0, 0.0, 1.0, 1.0,
96     ];
97     ushort[24] indices = [
98         2, 1, 0, 3, 2, 0,
99         4, 3, 0, 1, 4, 0,
100         5, 1, 2, 5, 2, 3,
101         5, 3, 4, 5, 4, 1,
102     ];
103     sg.BufferDesc vbufd = {
104         data: { ptr: vertices.ptr, size: vertices.sizeof },
105         label: "geometry-vbuf",
106     };
107     sg.BufferDesc ibufd = {
108         usage: { index_buffer: true },
109         data: { ptr: indices.ptr, size: indices.sizeof },
110         label: "geometry-ibuf",
111     };
112     state.display.bind.vertex_buffers[0] = sg.makeBuffer(vbufd);
113     state.display.bind.index_buffer = sg.makeBuffer(ibufd);
114 
115     // shader and pipeline for rendering the particles, this uses
116     // the compute-updated storage buffer to provide the particle positions
117     sg.PipelineDesc rpipd = {
118         shader: sg.makeShader(shd.displayShaderDesc(sg.queryBackend())),
119         layout: {
120             attrs: [
121                 shd.ATTR_DISPLAY_POS: { format: sg.VertexFormat.Float3 },
122                 shd.ATTR_DISPLAY_COLOR0: { format: sg.VertexFormat.Float4 },
123             ],
124         },
125         index_type: sg.IndexType.Uint16,
126         cull_mode: sg.CullMode.Back,
127         depth: {
128             compare: sg.CompareFunc.Less_equal,
129             write_enabled: true,
130         },
131         label: "render-pipeline",
132     };
133     state.display.pip = sg.makePipeline(rpipd);
134 
135     // one-time init of particle velocities via a compute shader
136     sg.PipelineDesc ipipd = {
137         compute: true,
138         shader: sg.makeShader(shd.initShaderDesc(sg.queryBackend())),
139     };
140     sg.Pipeline ipip = sg.makePipeline(ipipd);
141     sg.Pass pass = { compute: true };
142     sg.beginPass(pass);
143     sg.applyPipeline(ipip);
144     sg.applyBindings(state.compute.bind);
145     sg.dispatch(max_particles / 64, 1, 1);
146     sg.endPass();
147 }
148 
149 void frame() {
150     if (!sg.queryFeatures().compute) {
151         drawFallback();
152         return;
153     }
154 
155     state.num_particles += num_particles_emitted_per_frame;
156     if (state.num_particles > max_particles) {
157         state.num_particles = max_particles;
158     }
159     float dt = sapp.frameDuration();
160 
161     // compute pass to update particle positions
162     shd.CsParams cs_params = {
163         dt: dt,
164         num_particles: state.num_particles,
165     };
166     sg.Range cs_params_range = { ptr: &cs_params, size: cs_params.sizeof };
167     sg.Pass cpass = { compute: true, label: "compute-pass" };
168     sg.beginPass(cpass);
169     sg.applyPipeline(state.compute.pip);
170     sg.applyBindings(state.compute.bind);
171     sg.applyUniforms(shd.UB_CS_PARAMS, cs_params_range);
172     sg.dispatch((state.num_particles + 63) / 64, 1, 1);
173     sg.endPass();
174 
175     // render pass to render the particles via instancing, with the
176     // instance positions coming from the storage buffer
177     state.ry += 60.0 * dt;
178     shd.VsParams vs_params = computeVsParams(1.0, state.ry);
179     sg.Range vs_params_range = { ptr: &vs_params, size: vs_params.sizeof };
180     sg.Pass rpass = {
181         action: state.display.pass_action,
182         swapchain: sglue.swapchain(),
183         label: "render-pass",
184     };
185     sg.beginPass(rpass);
186     sg.applyPipeline(state.display.pip);
187     sg.applyBindings(state.display.bind);
188     sg.applyUniforms(shd.UB_VS_PARAMS, vs_params_range);
189     sg.draw(0, 24, state.num_particles);
190     sg.endPass();
191     sg.commit();
192 }
193 
194 void cleanup() {
195     sg.shutdown();
196 }
197 
198 void drawFallback() {
199     sg.Pass rpass = {
200         action: state.display.pass_action,
201         swapchain: sglue.swapchain(),
202         label: "render-pass",
203     };
204     sg.beginPass(rpass);
205     sg.endPass();
206     sg.commit();
207 }
208 
209 shd.VsParams computeVsParams(float rx, float ry) @nogc nothrow
210 {
211     immutable proj = Mat4.perspective(60.0, sapp.widthf() / sapp.heightf(), 0.01, 50.0);
212     immutable rxm = Mat4.rotate(rx, Vec3(1.0, 0.0, 0.0));
213     immutable rym = Mat4.rotate(ry, Vec3(0.0, 1.0, 0.0));
214     immutable model = Mat4.mul(rxm, rym);
215     immutable view_proj = Mat4.mul(proj, state.view());
216     shd.VsParams mvp = { mvp: Mat4.mul(view_proj, model) };
217     return mvp;
218 }
219 
220 void main() {
221     sapp.Desc adesc = {
222         init_cb: &init,
223         frame_cb: &frame,
224         cleanup_cb: &cleanup,
225         width: 800,
226         height: 600,
227         sample_count: 4,
228         window_title: "instancingcompute.d",
229         icon: { sokol_default: true },
230         logger: { func: &slog.func },
231     };
232     sapp.run(adesc);
233 }