1 /**
2 The widget you must inherit from for a PBR background UI (though it isn't mandatory).
3 
4 Copyright: Copyright Auburn Sounds 2015-2017.
5 License:   $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
6 Authors:   Guillaume Piolat
7 */
8 module dplug.pbrwidgets.pbrbackgroundgui;
9 
10 import dplug.math.box;
11 import dplug.core.nogc;
12 import dplug.core.file;
13 import dplug.core.thread;
14 
15 // Note: this dependency exist because Key is defined in dplug:window
16 import dplug.window.window;
17 
18 import dplug.gui.graphics;
19 import dplug.gui.element;
20 import dplug.gui.compositor;
21 import dplug.gui.legacypbr;
22 public import dplug.gui.sizeconstraints;
23 
24 import gamut;
25 
26 
27 // `decompressImagesLazily` cause JPEG and PNG to be decompressed on the fly on resize, instead 
28 // of ahead of time and staying in memory.
29 // This wins 17mb of RAM on Panagement.
30 // However, this also disable live reload of images for UI development. Hence, it is disabled for debug builds, in order
31 // to reload background with the RETURN key.
32 debug {}
33 else
34 {
35     version = decompressImagesLazily;
36 }
37 
38 /// PBRBackgroundGUI provides a PBR background loaded from PNG or JPEG images.
39 /// It's very practical while in development because it let's you reload the six
40 /// images used with the press of ENTER.
41 /// The path of each of these images (given as a template parameter) must be
42 /// in your "stringImportPaths" settings.
43 class PBRBackgroundGUI(string baseColorPath, 
44                        string emissivePath, 
45                        string materialPath,
46                        string depthPath,
47                        string skyboxPath,
48                        string absoluteGfxDirectory // for UI development only
49                        ) : GUIGraphics
50 {
51 public:
52 nothrow:
53 @nogc:
54 
55     this(int width, int height)
56     {
57         this(makeSizeConstraintsFixed(width, height));
58     }
59 
60     this(SizeConstraints sizeConstraints)
61     {
62         super(sizeConstraints, flagPBR | flagAnimated | flagDrawAlonePBR);
63 
64         _diffuseResized = mallocNew!(OwnedImage!RGBA);
65         _materialResized = mallocNew!(OwnedImage!RGBA);
66         _depthResized = mallocNew!(OwnedImage!L16);
67 
68         version(decompressImagesLazily)
69         {}
70         else
71         {
72             loadBackgroundImagesFromStaticData(null);
73         }
74 
75         auto skyboxData = cast(ubyte[])(import(skyboxPath));
76         loadSkybox(skyboxData);
77     }    
78 
79     ~this()
80     {
81         freeBackgroundImages();
82         _diffuseResized.destroyFree();
83         _materialResized.destroyFree();
84         _depthResized.destroyFree();
85     }
86 
87     // Development purposes. 
88     // In debug mode, pressing ENTER reload the backgrounds
89     debug
90     {
91         override bool onKeyDown(Key key)
92         {
93             if (super.onKeyDown(key))
94                 return true;
95 
96             version(decompressImagesLazily)
97             {
98             }
99             else
100             {
101                 if (key == Key.enter)
102                 {
103                     reloadImagesAtRuntime();
104                     return true;
105                 }
106             }
107 
108             return false;
109         }
110     }
111 
112     override void onDrawPBR(ImageRef!RGBA diffuseMap, ImageRef!L16 depthMap, ImageRef!RGBA materialMap, box2i[] dirtyRects)
113     {
114         // Resize resource to match _position
115         {
116             int W = position.width;
117             int H = position.height;
118 
119             if (_forceResizeUpdate || _diffuseResized.w != W || _diffuseResized.h != H)
120             {
121                 // Decompress images lazily for this size
122 
123                 version (decompressImagesLazily)
124                 {
125                     assert(_diffuse is null);
126 
127                     // No thread pool, load images one by one.
128                     loadBackgroundImagesFromStaticData(context.globalThreadPool);
129                 }
130 
131                 _diffuseResized.size(W, H);
132                 _materialResized.size(W, H);
133                 _depthResized.size(W, H);
134 
135                 // Potentially resize all 3 backgrounds in parallel 
136                 void resizeOneImage(int i, int threadIndex) nothrow @nogc
137                 {
138                     ImageResizer resizer;
139                     if (i == 0) 
140                     {
141                         version(Dplug_ProfileUI) context.profiler.begin("resize Diffuse background");
142                         resizer.resizeImageDiffuse(_diffuse.toRef, _diffuseResized.toRef);
143                         version(Dplug_ProfileUI) context.profiler.end;
144                     }
145                     if (i == 1) 
146                     {
147                         version(Dplug_ProfileUI) context.profiler.begin("resize Material background");
148                         resizer.resizeImageMaterial(_material.toRef, _materialResized.toRef);
149                         version(Dplug_ProfileUI) context.profiler.end;
150                     }
151                     if (i == 2) 
152                     {
153                         version(Dplug_ProfileUI) context.profiler.begin("resize Depth background");
154                         resizer.resizeImageDepth(_depth.toRef, _depthResized.toRef);
155                         version(Dplug_ProfileUI) context.profiler.end;
156                     }
157                 }
158                 context.globalThreadPool.parallelFor(3, &resizeOneImage);
159 
160                 _forceResizeUpdate = false;
161 
162                 version (decompressImagesLazily)
163                 {
164                     freeBackgroundImages();
165                     assert(_diffuse is null);
166                 }
167             }
168         }
169 
170         // Just blit backgrounds into dirtyRects.
171         foreach(dirtyRect; dirtyRects)
172         {
173             auto croppedDiffuseIn = _diffuseResized.toRef().cropImageRef(dirtyRect);
174             auto croppedDiffuseOut = diffuseMap.cropImageRef(dirtyRect);
175 
176             auto croppedDepthIn = _depthResized.toRef().cropImageRef(dirtyRect);
177             auto croppedDepthOut = depthMap.cropImageRef(dirtyRect);
178 
179             auto croppedMaterialIn = _materialResized.toRef().cropImageRef(dirtyRect);
180             auto croppedMaterialOut = materialMap.cropImageRef(dirtyRect);
181 
182             croppedDiffuseIn.blitTo(croppedDiffuseOut);
183             croppedDepthIn.blitTo(croppedDepthOut);
184             croppedMaterialIn.blitTo(croppedMaterialOut);
185         }
186     }
187 
188     override void reflow()
189     {
190     }
191 
192 private:
193 
194     // Can pass a ThreadPool in the case you want parallel image loading (optional).
195     final void loadBackgroundImagesFromStaticData(ThreadPool* threadPool)
196     {
197         auto basecolorData = cast(ubyte[])(import(baseColorPath));
198         static if (emissivePath)
199             ubyte[] emissiveData = cast(ubyte[])(import(emissivePath));
200         else
201             ubyte[] emissiveData = null;
202         auto materialData = cast(ubyte[])(import(materialPath));
203         auto depthData = cast(ubyte[])(import(depthPath));
204 
205         loadBackgroundImages(basecolorData, 
206                              emissiveData, 
207                              materialData, 
208                              depthData,
209                              threadPool);
210     }
211 
212     // CTFE used here so we are allowed to use ~
213     static immutable string baseColorPathAbs = absoluteGfxDirectory ~ baseColorPath;
214     static immutable string emissivePathAbs = emissivePath ? (absoluteGfxDirectory ~ emissivePath) : null;
215     static immutable string materialPathAbs = absoluteGfxDirectory ~ materialPath;
216     static immutable string depthPathAbs = absoluteGfxDirectory ~ depthPath;
217     static immutable string skyboxPathAbs = absoluteGfxDirectory ~ skyboxPath;
218 
219     OwnedImage!RGBA _diffuse;
220     OwnedImage!RGBA _material;
221     OwnedImage!L16 _depth;
222 
223     OwnedImage!RGBA _diffuseResized;
224     OwnedImage!RGBA _materialResized;
225     OwnedImage!L16 _depthResized;
226 
227     /// Where pixel data is taken in the image, expressed in _background coordinates.
228     box2i _sourceRect;
229 
230     /// Where it is deposited. Same size than _sourceRect. Expressed in _position coordinates.
231     box2i _destRect;
232 
233     /// Offset from source to dest.
234     vec2i _offset;
235 
236     /// Force resize of source image in order to display changes while editing files.
237     bool _forceResizeUpdate;
238 
239     void freeBackgroundImages()
240     {
241         if (_diffuse)
242         {
243             _diffuse.destroyFree();
244             _diffuse = null;
245         }
246 
247         if (_depth)
248         {
249             _depth.destroyFree();
250             _depth = null;
251         }
252 
253         if (_material)
254         {
255             _material.destroyFree();
256             _material = null;
257         }
258     }
259 
260     version(decompressImagesLazily)
261     {
262     }
263     else
264     {
265         // Reloads images for UI development, avoid long compile round trips
266         // This saves up hours.
267         void reloadImagesAtRuntime()
268         {
269             // reading images with an absolute path since we don't know 
270             // which is the current directory from the host
271             ubyte[] basecolorData = readFile(baseColorPathAbs);
272             ubyte[] emissiveData = emissivePathAbs ? readFile(emissivePathAbs) : null;
273             ubyte[] materialData = readFile(materialPathAbs);
274             ubyte[] depthData = readFile(depthPathAbs);
275             ubyte[] skyboxData = readFile(skyboxPathAbs);
276 
277             if (basecolorData && materialData
278                 && depthData && skyboxData) // all valid?
279             {
280                 // Reload images from disk and update the UI
281                 freeBackgroundImages();
282                 loadBackgroundImages(basecolorData, emissiveData, materialData, depthData, null);
283                 loadSkybox(skyboxData);
284                 _forceResizeUpdate = true;
285                 setDirtyWhole();
286             }
287             else
288             {
289                 // Note: if you fail here, the absolute path you provided in your gui.d was incorrect.
290                 // The background files cannot be loaded at runtime, and you have to fix your pathes.
291                 assert(false);
292             }
293 
294             // Release copy of file contents
295             freeSlice(basecolorData);
296             freeSlice(emissiveData);
297             freeSlice(materialData);
298             freeSlice(depthData);
299             freeSlice(skyboxData);
300         }
301     }
302 
303     void loadBackgroundImages(ubyte[] basecolorData, 
304                               ubyte[] emissiveData, // this one can be null
305                               ubyte[] materialData, 
306                               ubyte[] depthData,
307                               ThreadPool* threadPool)
308     {
309         // Potentially load all 3 background images in parallel, if one threadPool is provided.
310         void loadOneImage(int i, int threadIndex) nothrow @nogc
311         {
312             ImageResizer resizer;
313             if (i == 0) 
314             {
315                 version(Dplug_ProfileUI) context.profiler.category("image").begin("load Diffuse background");
316                 if (emissiveData)
317                     _diffuse = loadImageSeparateAlpha(basecolorData, emissiveData);
318                 else
319                 {
320                     // fill with zero, no emissive data => zero Emissive
321                     _diffuse = loadImageWithFilledAlpha(basecolorData, 0);
322                 }
323                 version(Dplug_ProfileUI) context.profiler.end;
324             }
325             if (i == 1) 
326             {
327                 version(Dplug_ProfileUI) context.profiler.begin("load Material background");
328                 _material = loadOwnedImage(materialData);
329                 version(Dplug_ProfileUI) context.profiler.end;
330             }
331             if (i == 2) 
332             {
333                 version(Dplug_ProfileUI) context.profiler.begin("load Depth background");
334                 _depth = loadOwnedImageDepth(depthData);
335                 version(Dplug_ProfileUI) context.profiler.end;
336             }
337         }
338         if (threadPool)
339         {
340             threadPool.parallelFor(3, &loadOneImage);
341         }
342         else
343         {
344             loadOneImage(0, -1);
345             loadOneImage(1, -1);
346             loadOneImage(2, -1);
347         }       
348     }
349     
350     void loadSkybox(ubyte[] skyboxData)
351     {        
352         // Search for a pass of type PassSkyboxReflections
353         if (auto mpc = cast(MultipassCompositor) compositor())
354         {
355             foreach(pass; mpc.passes())
356             {
357                 if (auto skyreflPass = cast(PassSkyboxReflections)pass)
358                 {
359                     version(Dplug_ProfileUI) context.profiler.category("image").begin("load Skybox");
360                     OwnedImage!RGBA skybox = loadOwnedImage(skyboxData);
361                     skyreflPass.setSkybox(skybox);
362                     version(Dplug_ProfileUI) context.profiler.end;
363                 }
364             }
365         }
366     }
367 }
368