1 /**
2 * PBR rendering, custom rendering.
3 *
4 * Copyright: Copyright Auburn Sounds 2015 and later.
5 * License:   $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
6 * Authors:   Guillaume Piolat
7 */
8 module dplug.gui.compositor;
9 
10 import core.stdc.stdio;
11 
12 import dplug.math.box;
13 
14 import dplug.core.vec;
15 import dplug.core.nogc;
16 import dplug.core.thread;
17 
18 import dplug.graphics;
19 
20 import dplug.window.window;
21 import dplug.gui.profiler;
22 
23 
24 /// Only deals with rendering tiles.
25 /// If you don't like Dplug default compositing, just make another Compositor
26 /// and assign the 'compositor' field in GUIGraphics.
27 /// However for now mipmaps are not negotiable, they will get generated outside this compositor.
28 interface ICompositor
29 {
30 nothrow:
31 @nogc:
32     /// Setup the compositor to output a particular output size.
33     ///
34     /// Params:
35     ///     width      The width of the given input mipmaps, and the width of the output image.
36     ///     height     The height of the given input mipmaps, and the height of the output image.
37     ///     areaMaxWidth  The maximum width of the `area` passed in `compositeTile`. This is useful to allocate smaller thread-local buffers.
38     ///     areaMaxHeight The maximum height of the `area` passed in `compositeTile`. This is useful to allocate smaller thread-local buffers.
39     ///
40     /// Note: this call is always called before `compositeTile` is called, and never simultaneously.
41     ///
42     void resizeBuffers(int width, 
43                        int height,
44                        int areaMaxWidth,
45                        int areaMaxHeight);
46 
47     /// From given input mipmaps, write output image into `wfb` with pixel format `pf`, for the output area `area`.
48     ///
49     /// Params:
50     ///     wfb         Image to write the output pixels to.
51     ///     diffuseMap  Diffuse input.  Basecolor + Emissive, the Compositor decides how it uses these channels.
52     ///     materialMap Material input. Roughness + Metalness + Specular + Unused, the Compositor decides how it uses these channels.
53     ///     depthMap    Depth input. A different of `FACTOR_Z` in the Z direction has a similar size as a displacement of one pixels.
54     ///                 As such, the range of possible simulated depth is (ushort.max / FACTOR_Z) pixels.
55     ///                 But ultimately, the Compositor decides how it uses these channels.
56     ///     skybox      Environment texture. Cheap and dirty reflections, to simulate metals.
57     ///
58     /// Note: several threads will call this function concurrently. 
59     ///       It is important to only deal with `area` to avoid clashing writes.
60     ///
61     void compositeTile(ImageRef!RGBA wfb, 
62                        const(box2i)[] areas,
63                        Mipmap!RGBA diffuseMap,
64                        Mipmap!RGBA materialMap,
65                        Mipmap!L16 depthMap,
66                        IProfiler profiler);
67 }
68 
69 /// What a compositor is passed upon creation within `GUIGraphics`.
70 struct CompositorCreationContext
71 {
72     /// The thread instance this compositor is allowed to use while in `compositeTile`.
73     ThreadPool threadPool;
74 }
75 
76 
77 /// Compositor with series of successive passes.
78 /// This owns an arbitrary number of passesn that are created in its constructor.
79 class MultipassCompositor : ICompositor
80 {
81 public:
82 nothrow:
83 @nogc:
84 
85     /// Params:
86     ///     threadPool The thread instance this compositor is allowed to use while in `compositeTile`.
87     ///
88     this(CompositorCreationContext* context)
89     {
90         _threadPool = context.threadPool;
91 
92         // override, call `super(threadPool)`, and add passes here with `addPass`. 
93         // They will be `destroyFree` on exit.
94     }
95 
96     /// Enqueue a pass in the compositor pipeline.
97     /// This is meant to be used in a `MultipassCompositor` derivative constructor.
98     /// Passes are called in their order of addition.
99     /// That pass is now owned by the `MultipassCompositor`.
100     protected void addPass(CompositorPass pass)
101     {
102         _passes.pushBack(pass);
103     }
104 
105     override void resizeBuffers(int width, 
106                                 int height,
107                                 int areaMaxWidth,
108                                 int areaMaxHeight)
109     {
110         foreach(pass; _passes)
111         {
112             pass.resizeBuffers(width, height, areaMaxWidth, areaMaxHeight);
113         }
114     }
115 
116     /// Note: the exact algorithm for compositing pass is entirely up to you.
117     /// You could imagine intermediate mipmappingsteps in the middle.
118     /// `compositeTile` needs complete power over the parallelization strategy.
119     override void compositeTile(ImageRef!RGBA wfb, 
120                                 const(box2i)[] areas,
121                                 Mipmap!RGBA diffuseMap,
122                                 Mipmap!RGBA materialMap,
123                                 Mipmap!L16 depthMap,
124                                 IProfiler profiler)
125     {
126         // Note: if you want to customize rendering further, you can add new buffers to a struct extending
127         // CompositorPassBuffers, override `compositeTile` and you will still be able to use the former passes.
128         CompositorPassBuffers buffers;
129         buffers.outputBuf = &wfb;
130         buffers.diffuseMap = diffuseMap;
131         buffers.materialMap = materialMap;
132         buffers.depthMap = depthMap;
133 
134         // For each tile, do all pass one by one.
135         void compositeOneTile(int i, int threadIndex) nothrow @nogc
136         {
137             foreach(pass; _passes)
138             {
139                 version(Dplug_ProfileUI) 
140                 {
141                     char[96] buf;
142                     snprintf(buf.ptr, 96, "Pass %s".ptr, pass.name.ptr);
143                     profiler.category("ui").begin(buf);
144                 }
145                 
146                 pass.renderIfActive(threadIndex, areas[i], &buffers);
147 
148                 version(Dplug_ProfileUI) 
149                 {
150                     profiler.end();
151                 }
152             }
153         }
154         int numAreas = cast(int)areas.length;
155         _threadPool.parallelFor(numAreas, &compositeOneTile);    
156     }
157 
158     ~this()
159     {
160         foreach(pass; _passes[])
161         {
162             destroyFree(pass);
163         }
164     }
165 
166     final int numThreads()
167     {
168         return _threadPool.numThreads();
169     }
170 
171     final inout(CompositorPass) getPass(int nth) inout
172     {
173         return _passes[nth];
174     }
175 
176     final inout(CompositorPass)[] passes() inout
177     {
178         return _passes[];
179     }
180 
181 protected:
182     ref ThreadPool threadPool()
183     {
184         return _threadPool;
185     }
186 
187 private:
188 
189     // Thread pool that could be used to speed-up things.
190     // This thread pool must outlive the compositor.
191     ThreadPool _threadPool;
192 
193     // Implements ICompositor
194     Vec!CompositorPass _passes;
195 }
196 
197 struct CompositorPassBuffers
198 {
199     ImageRef!RGBA* outputBuf;
200     Mipmap!RGBA diffuseMap;
201     Mipmap!RGBA materialMap;
202     Mipmap!L16 depthMap;
203 }
204 
205 
206 /// Derive from this class to create new passes.
207 class CompositorPass
208 {
209 public:
210 nothrow:
211 @nogc:
212 
213     string name()
214     {
215         return typeid(this).name;
216     }
217 
218     this(MultipassCompositor parent)
219     {
220         _numThreads = parent.numThreads();
221     }
222 
223     final void setActive(bool active)
224     {
225         _active = active;
226     }
227 
228     final int numThreads()
229     {
230         return _numThreads;
231     }
232 
233     final void renderIfActive(int threadIndex, const(box2i) area, CompositorPassBuffers* buffers)
234     {
235         if (_active)
236             render(threadIndex, area, buffers);
237     }
238 
239     /// Override this to allocate temporary buffers, eventually.
240     void resizeBuffers(int width, int height, int areaMaxWidth, int areaMaxHeight)
241     {
242         // do nothing by default
243     }
244 
245     /// Override this to specify what the pass does.
246     /// If you need more buffers, use type-punning based on a `CompositorPassBuffers` extension.
247     abstract void render(int threadIndex, const(box2i) area, CompositorPassBuffers* buffers);
248 
249 private:
250 
251     /// The compositor that owns this pass.
252     int _numThreads;
253 
254     /// Whether the pass should execute on render. This breaks dirtiness if you change it.
255     bool _active = true;
256 }
257 
258 
259 /// Example Compositor implementation; this just copies the diffuse map into
260 /// and is useful for flat plugins.
261 final class SimpleRawCompositor : MultipassCompositor
262 {
263 nothrow:
264 @nogc:
265     this(CompositorCreationContext* context)
266     {
267         super(context);
268         addPass(mallocNew!CopyDiffuseToFramebuffer(this));
269     }
270 
271     static class CopyDiffuseToFramebuffer : CompositorPass
272     {
273     nothrow:
274     @nogc:
275 
276         this(MultipassCompositor parent)
277         {
278             super(parent);
279         }
280 
281         override void render(int threadIndex, const(box2i) area, CompositorPassBuffers* buffers)
282         {
283             ImageRef!RGBA outputBuf = *(buffers.outputBuf);
284             OwnedImage!RGBA diffuseBuf = buffers.diffuseMap.levels[0];
285             for (int j = area.min.y; j < area.max.y; ++j)
286             {
287                 RGBA* wfb_scan = outputBuf.scanline(j).ptr;
288                 RGBA* diffuseScan = diffuseBuf.scanlinePtr(j);
289 
290                 // write composited color
291                 for (int i = area.min.x; i < area.max.x; ++i)
292                 {
293                     RGBA df = diffuseScan[i];
294                     wfb_scan[i] = RGBA(df.r, df.g, df.b, 255);
295                 }
296             }
297         }
298     }
299 }