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 }