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     // If this version is enabled, you could press ENTER to
89     // reload the backgrounds. Do not ship this!
90     version(Dplug_EnterReloadBackgrounds)
91     {
92         override bool onKeyDown(Key key)
93         {
94             if (super.onKeyDown(key))
95                 return true;
96 
97             version(decompressImagesLazily)
98             {
99             }
100             else
101             {
102                 if (key == Key.enter)
103                 {
104                     reloadImagesAtRuntime();
105                     return true;
106                 }
107             }
108 
109             return false;
110         }
111     }
112 
113     override void onDrawPBR(ImageRef!RGBA diffuseMap, ImageRef!L16 depthMap, ImageRef!RGBA materialMap, box2i[] dirtyRects)
114     {
115         // Resize resource to match _position
116         {
117             int W = position.width;
118             int H = position.height;
119 
120             if (_forceResizeUpdate || _diffuseResized.w != W || _diffuseResized.h != H)
121             {
122                 // Decompress images lazily for this size
123 
124                 version (decompressImagesLazily)
125                 {
126                     assert(_diffuse is null);
127 
128                     // No thread pool, load images one by one.
129                     loadBackgroundImagesFromStaticData(context.globalThreadPool);
130                 }
131 
132                 _diffuseResized.size(W, H);
133                 _materialResized.size(W, H);
134                 _depthResized.size(W, H);
135 
136                 // Potentially resize all 3 backgrounds in parallel
137                 void resizeOneImage(int i, int threadIndex) nothrow @nogc
138                 {
139                     ImageResizer resizer;
140                     if (i == 0)
141                     {
142                         version(Dplug_ProfileUI) context.profiler.begin("resize Diffuse background");
143                         resizer.resizeImageDiffuse(_diffuse.toRef, _diffuseResized.toRef);
144                         version(Dplug_ProfileUI) context.profiler.end;
145                     }
146                     if (i == 1)
147                     {
148                         version(Dplug_ProfileUI) context.profiler.begin("resize Material background");
149                         resizer.resizeImageMaterial(_material.toRef, _materialResized.toRef);
150                         version(Dplug_ProfileUI) context.profiler.end;
151                     }
152                     if (i == 2)
153                     {
154                         version(Dplug_ProfileUI) context.profiler.begin("resize Depth background");
155                         resizer.resizeImageDepth(_depth.toRef, _depthResized.toRef);
156                         version(Dplug_ProfileUI) context.profiler.end;
157                     }
158                 }
159                 context.globalThreadPool.parallelFor(3, &resizeOneImage);
160 
161                 _forceResizeUpdate = false;
162 
163                 version (decompressImagesLazily)
164                 {
165                     freeBackgroundImages();
166                     assert(_diffuse is null);
167                 }
168             }
169         }
170 
171         // Just blit backgrounds into dirtyRects.
172         foreach(dirtyRect; dirtyRects)
173         {
174             auto croppedDiffuseIn = _diffuseResized.toRef().cropImageRef(dirtyRect);
175             auto croppedDiffuseOut = diffuseMap.cropImageRef(dirtyRect);
176 
177             auto croppedDepthIn = _depthResized.toRef().cropImageRef(dirtyRect);
178             auto croppedDepthOut = depthMap.cropImageRef(dirtyRect);
179 
180             auto croppedMaterialIn = _materialResized.toRef().cropImageRef(dirtyRect);
181             auto croppedMaterialOut = materialMap.cropImageRef(dirtyRect);
182 
183             croppedDiffuseIn.blitTo(croppedDiffuseOut);
184             croppedDepthIn.blitTo(croppedDepthOut);
185             croppedMaterialIn.blitTo(croppedMaterialOut);
186         }
187     }
188 
189     override void reflow()
190     {
191     }
192 
193 private:
194 
195     // Can pass a ThreadPool in the case you want parallel image loading (optional).
196     final void loadBackgroundImagesFromStaticData(ThreadPool* threadPool)
197     {
198         auto basecolorData = cast(ubyte[])(import(baseColorPath));
199         static if (emissivePath)
200             ubyte[] emissiveData = cast(ubyte[])(import(emissivePath));
201         else
202             ubyte[] emissiveData = null;
203         auto materialData = cast(ubyte[])(import(materialPath));
204         auto depthData = cast(ubyte[])(import(depthPath));
205 
206         loadBackgroundImages(basecolorData,
207                              emissiveData,
208                              materialData,
209                              depthData,
210                              threadPool);
211     }
212 
213     // CTFE used here so we are allowed to use ~
214     static immutable string baseColorPathAbs = absoluteGfxDirectory ~ baseColorPath;
215     static immutable string emissivePathAbs = emissivePath ? (absoluteGfxDirectory ~ emissivePath) : null;
216     static immutable string materialPathAbs = absoluteGfxDirectory ~ materialPath;
217     static immutable string depthPathAbs = absoluteGfxDirectory ~ depthPath;
218     static immutable string skyboxPathAbs = absoluteGfxDirectory ~ skyboxPath;
219 
220     OwnedImage!RGBA _diffuse;
221     OwnedImage!RGBA _material;
222     OwnedImage!L16 _depth;
223 
224     OwnedImage!RGBA _diffuseResized;
225     OwnedImage!RGBA _materialResized;
226     OwnedImage!L16 _depthResized;
227 
228     /// Where pixel data is taken in the image, expressed in _background coordinates.
229     box2i _sourceRect;
230 
231     /// Where it is deposited. Same size than _sourceRect. Expressed in _position coordinates.
232     box2i _destRect;
233 
234     /// Offset from source to dest.
235     vec2i _offset;
236 
237     /// Force resize of source image in order to display changes while editing files.
238     bool _forceResizeUpdate;
239 
240     void freeBackgroundImages()
241     {
242         if (_diffuse)
243         {
244             _diffuse.destroyFree();
245             _diffuse = null;
246         }
247 
248         if (_depth)
249         {
250             _depth.destroyFree();
251             _depth = null;
252         }
253 
254         if (_material)
255         {
256             _material.destroyFree();
257             _material = null;
258         }
259     }
260 
261     version(decompressImagesLazily)
262     {
263     }
264     else
265     {
266         // Reloads images for UI development, avoid long compile round trips
267         // This saves up hours.
268         void reloadImagesAtRuntime()
269         {
270             // reading images with an absolute path since we don't know
271             // which is the current directory from the host
272             ubyte[] basecolorData = readFile(baseColorPathAbs);
273             ubyte[] emissiveData = emissivePathAbs ? readFile(emissivePathAbs) : null;
274             ubyte[] materialData = readFile(materialPathAbs);
275             ubyte[] depthData = readFile(depthPathAbs);
276             ubyte[] skyboxData = readFile(skyboxPathAbs);
277 
278             if (basecolorData && materialData
279                 && depthData && skyboxData) // all valid?
280             {
281                 // Reload images from disk and update the UI
282                 freeBackgroundImages();
283                 loadBackgroundImages(basecolorData, emissiveData, materialData, depthData, null);
284                 loadSkybox(skyboxData);
285                 _forceResizeUpdate = true;
286                 setDirtyWhole();
287             }
288             else
289             {
290                 // Note: if you fail here, the absolute path you provided in your gui.d was incorrect.
291                 // The background files cannot be loaded at runtime, and you have to fix your pathes.
292                 assert(false);
293             }
294 
295             // Release copy of file contents
296             freeSlice(basecolorData);
297             freeSlice(emissiveData);
298             freeSlice(materialData);
299             freeSlice(depthData);
300             freeSlice(skyboxData);
301         }
302     }
303 
304     void loadBackgroundImages(ubyte[] basecolorData,
305                               ubyte[] emissiveData, // this one can be null
306                               ubyte[] materialData,
307                               ubyte[] depthData,
308                               ThreadPool* threadPool)
309     {
310         // Potentially load all 3 background images in parallel, if one threadPool is provided.
311         void loadOneImage(int i, int threadIndex) nothrow @nogc
312         {
313             ImageResizer resizer;
314             if (i == 0)
315             {
316                 version(Dplug_ProfileUI) context.profiler.category("image").begin("load Diffuse background");
317                 if (emissiveData)
318                     _diffuse = loadImageSeparateAlpha(basecolorData, emissiveData);
319                 else
320                 {
321                     // fill with zero, no emissive data => zero Emissive
322                     _diffuse = loadImageWithFilledAlpha(basecolorData, 0);
323                 }
324                 version(Dplug_ProfileUI) context.profiler.end;
325             }
326             if (i == 1)
327             {
328                 version(Dplug_ProfileUI) context.profiler.begin("load Material background");
329                 _material = loadOwnedImage(materialData);
330                 version(Dplug_ProfileUI) context.profiler.end;
331             }
332             if (i == 2) 
333             {
334                 version(Dplug_ProfileUI) context.profiler.begin("load Depth background");
335                 _depth = loadOwnedImageDepth(depthData);
336                 version(Dplug_ProfileUI) context.profiler.end;
337             }
338         }
339         if (threadPool)
340         {
341             threadPool.parallelFor(3, &loadOneImage);
342         }
343         else
344         {
345             loadOneImage(0, -1);
346             loadOneImage(1, -1);
347             loadOneImage(2, -1);
348         }
349     }
350 
351     void loadSkybox(ubyte[] skyboxData)
352     {
353         // Search for a pass of type PassSkyboxReflections
354         if (auto mpc = cast(MultipassCompositor) compositor())
355         {
356             foreach(pass; mpc.passes())
357             {
358                 if (auto skyreflPass = cast(PassSkyboxReflections)pass)
359                 {
360                     version(Dplug_ProfileUI) context.profiler.category("image").begin("load Skybox");
361                     OwnedImage!RGBA skybox = loadOwnedImage(skyboxData);
362                     skyreflPass.setSkybox(skybox);
363                     version(Dplug_ProfileUI) context.profiler.end;
364                 }
365             }
366         }
367     }
368 }
369