1 /**
2     Inochi2D Textures
3 
4     Copyright: 
5         Copyright © 2020-2026, Inochi2D Project
6     
7     License:
8         $(LINK2 https://github.com/Inochi2D/inochi2d/blob/main/LICENSE, BSD 2-clause License)
9     
10     Authors:
11         Luna Nielsen
12 */
13 module inochi2d.core.render.texture;
14 import inochi2d.core.render.resource;
15 import nulib.io.stream;
16 import numath;
17 import nulib;
18 import numem;
19 import gamut;
20 
21 /**
22     Format of texture data.
23 */
24 enum TextureFormat : uint {
25 
26     /**
27         None or unknown encoding.
28     */
29     none = 0,
30 
31     /**
32         RGBA8 data.
33     */
34     rgba8Unorm = 1,
35 
36     /**
37         Red-channel only mask data.
38     */
39     r8 = 2
40 }
41 
42 /**
43     A texture.
44 */
45 class Texture : Resource {
46 public:
47 @nogc:
48 
49     /**
50         Texture data.
51     */
52     TextureData data;
53 
54     /**
55         Creates a new texture.
56 
57         Params:
58             width = The requested width of the texture,
59             height = The requested height of the texture,
60             format = The requested format of the texture,
61     */
62     static Texture create(uint width, uint height, TextureFormat format) {
63         return nogc_new!Texture(width, height, format);
64     }
65 
66     /**
67         Creates a new texture with the given texture data.
68 
69         Params:
70             data = The data to use for creation.
71     */
72     static Texture createForData(TextureData data) {
73         return nogc_new!Texture(data);
74     }
75 
76     /**
77         Length of the resource's data allocation in bytes.
78     */
79     override @property uint length() => cast(uint)data.data.length;
80 
81     /**
82         Format of the texture.
83     */
84     final @property TextureFormat format() => data.format;
85 
86     /**
87         Width of the texture in pixels.
88     */
89     final @property uint width() => data.width;
90 
91     /**
92         Height of the texture in pixels.
93     */
94     final @property uint height() => data.height;
95 
96     /**
97         Channel count of the texture.
98     */
99     final @property uint channels() => data.channels;
100 
101     /**
102         Pixel data of the texture.
103     */
104     final @property void[] pixels() => data.data;
105 
106     // Destructor
107     ~this() {
108         data.free();
109     }
110 
111     /**
112         Constructs a new texture.
113     */
114     this(uint width, uint height, TextureFormat format) {
115         data.width = width;
116         data.height = height;
117         data.format = format;
118     }
119 
120     /**
121         Constructs a new texture.
122     */
123     this(TextureData data) {
124         this.data = data;
125     }
126 
127     /**
128         Resizes the texture.
129     */
130     void resize(uint width, uint height) {
131         data.resize(width, height);
132     }
133 }
134 
135 /**
136     Texture Data used during GPU uploads.
137 */
138 struct TextureData {
139 public:
140 @nogc:
141     uint width;
142     uint height;
143     TextureFormat format;
144     void[] data;
145 
146     /**
147         Amount of color channels in the image.
148     */
149     @property uint channels() {
150         final switch (format) {
151         case TextureFormat.rgba8Unorm:
152             return 4;
153 
154         case TextureFormat.r8:
155             return 1;
156 
157         case TextureFormat.none:
158             return 0;
159         }
160     }
161 
162     static TextureData load(ubyte[] data) {
163         TextureData result;
164         try {
165             Image img;
166             if (!img.loadFromMemory(data, LAYOUT_GAPLESS | LAYOUT_VERT_STRAIGHT | LOAD_8BIT | LOAD_NO_PREMUL))
167                 throw nogc_new!NuException(img.errorMessage());
168 
169             switch (img.type) with (PixelType) {
170             case unknown:
171                 throw nogc_new!NuException("Unknown pixel format for texture!");
172 
173             case l8:
174                 result.format = TextureFormat.r8;
175                 break;
176 
177             case rgba8:
178                 result.format = TextureFormat.rgba8Unorm;
179                 break;
180 
181             default:
182                 img.convertTo(PixelType.rgba8, LAYOUT_GAPLESS | LAYOUT_VERT_STRAIGHT);
183                 result.format = TextureFormat.rgba8Unorm;
184                 break;
185             }
186 
187             result.width = img.width();
188             result.height = img.height();
189             result.data = nu_dup(img.allPixelsAtOnce());
190             return result;
191         } catch (Exception ex) {
192             throw ex;
193         }
194     }
195 
196     /**
197         Premultiplies incoming color data.
198     */
199     void premultiply() {
200         final switch (format) {
201         case TextureFormat.rgba8Unorm:
202             ubyte[] dataView = cast(ubyte[])data;
203             foreach (i; 0 .. data.length / 4) {
204                 size_t offsetPixel = (i * 4);
205 
206                 float r = (cast(float)dataView[offsetPixel + 0] / 255.0) * (cast(float)dataView[offsetPixel + 3] / 255.0);
207                 float g = (cast(float)dataView[offsetPixel + 1] / 255.0) * (cast(float)dataView[offsetPixel + 3] / 255.0);
208                 float b = (cast(float)dataView[offsetPixel + 2] / 255.0) * (cast(float)dataView[offsetPixel + 3] / 255.0);
209 
210                 dataView[offsetPixel + 0] = cast(ubyte)(r * 255.0);
211                 dataView[offsetPixel + 1] = cast(ubyte)(g * 255.0);
212                 dataView[offsetPixel + 2] = cast(ubyte)(b * 255.0);
213             }
214             return;
215 
216         case TextureFormat.none:
217         case TextureFormat.r8:
218             return;
219         }
220     }
221 
222     /**
223         Un-premultiplies incoming color data.
224     */
225     void unpremultiply() {
226         final switch (format) {
227         case TextureFormat.rgba8Unorm:
228             ubyte[] dataView = cast(ubyte[])data;
229             foreach (i; 0 .. data.length / 4) {
230 
231                 size_t offsetPixel = (i * 4);
232 
233                 // Ensure no divide by zero happens.
234                 if (cast(float)dataView[offsetPixel + 3] == 0) {
235                     dataView[offsetPixel .. offsetPixel + 3] = 0;
236                     continue;
237                 }
238 
239                 float r = (cast(float)dataView[offsetPixel + 0] / 255.0) / (cast(float)dataView[offsetPixel + 3] / 255.0);
240                 float g = (cast(float)dataView[offsetPixel + 1] / 255.0) / (cast(float)dataView[offsetPixel + 3] / 255.0);
241                 float b = (cast(float)dataView[offsetPixel + 2] / 255.0) / (cast(float)dataView[offsetPixel + 3] / 255.0);
242                 dataView[offsetPixel + 0] = cast(ubyte)(r * 255.0);
243                 dataView[offsetPixel + 1] = cast(ubyte)(g * 255.0);
244                 dataView[offsetPixel + 2] = cast(ubyte)(b * 255.0);
245             }
246             return;
247 
248         case TextureFormat.none:
249         case TextureFormat.r8:
250             return;
251         }
252     }
253 
254     /**
255         Dumps the image data to the specified file.
256 
257         Params:
258             file = The file to dump the texture data to.
259     */
260     void dump(string file) {
261         if (data.length > 0) {
262             Image img;
263             img.createView(data.ptr, width, height, format.toPixelType, width * channels);
264             img.saveToFile(file);
265         }
266     }
267 
268     /**
269         Pads the texture with a 1-pixel wide border.
270 
271         Params:
272             thickness = The border thickness to generate.
273     */
274     void pad(uint thickness) {
275         if (data.length == 0)
276             return;
277 
278         uint totalPad = thickness * 2;
279         ubyte[] newData = nu_malloca!ubyte((width + totalPad) * (height + totalPad) * channels);
280         newData[0 .. $] = 0;
281 
282         size_t srcStride = width * channels;
283         size_t dstStride = (width + totalPad) * channels;
284         foreach (y; 0 .. height) {
285             size_t start = (dstStride * (y + thickness)) + (thickness * channels);
286             size_t end = start + srcStride;
287             newData[start .. end] = cast(ubyte[])data[srcStride * y .. (srcStride * y) + srcStride];
288         }
289 
290         // Update the texture
291         nu_freea(data);
292         this.data = newData;
293         this.width = width + totalPad;
294         this.height = height + totalPad;
295     }
296 
297     /**
298         Resizes the texture data, ensuring that if any data is supplied
299         it is updated to fit within the new target size.
300     */
301     void resize(uint width, uint height) {
302         if (data.length > 0) {
303             void[] newData = nu_malloca!ubyte(width * height * channels);
304 
305             // Copy as many horizontal lines as requested
306             // into our new buffer.
307             size_t oldStride = this.width * channels;
308             size_t newStride = width * channels;
309             size_t cStride = min(oldStride, newStride);
310             foreach (y; 0 .. min(this.height, height)) {
311                 newData[newStride * y .. (newStride * y) + newStride] = data[oldStride * y .. (oldStride * y) + cStride];
312             }
313 
314             // Data has been copied over, now replace the old array.
315             nu_freea(data);
316             data = newData;
317         }
318 
319         this.width = width;
320         this.height = height;
321     }
322 
323     /**
324         Flip the texture vertically.
325     */
326     void vflip() {
327         if (data.length > 0) {
328             size_t stride = width * channels;
329             void[] tmp = nu_malloca!ubyte(stride);
330             foreach (y; 0 .. height / 2) {
331                 void[] top = data[stride * y .. (stride * y) + stride];
332                 void[] bottom = data[stride * (height - (y + 1)) .. (stride * (height - (y + 1))) + stride];
333 
334                 tmp[0 .. stride] = top[0 .. stride];
335                 top[0 .. stride] = bottom[0 .. stride];
336                 bottom[0 .. stride] = tmp[0 .. stride];
337             }
338         }
339     }
340 
341     /**
342         Frees the texture and all the data associated with it.
343 
344         This does not free any data that has been transferred to
345         the GPU.
346     */
347     void free() {
348         nu_freea(data);
349     }
350 }
351 
352 /**
353     A cache of textures in use by a model.
354 */
355 final
356 class TextureCache : NuObject {
357 private:
358 @nogc:
359     vector!Texture textures;
360 
361 public:
362 
363     // Destructor
364      ~this() {
365         foreach (ref texture; textures) {
366             texture.release();
367         }
368         textures.clear();
369     }
370 
371     /**
372         Size of the texture cache in elements.
373     */
374     @property size_t size() => textures.length;
375 
376     /**
377         The cached textures.
378     */
379     @property Texture[] cache() => textures[0 .. $];
380 
381     /**
382         Adds a texture to the cache, adding a retain count
383         to the texture. Texture caches only allow a single
384         instance of a texture to be stored within.
385 
386         Params:
387             texture = The texture to add to the cache.
388 
389         Returns:
390             The texture slot position of the added texture.
391     */
392     uint add(Texture texture) {
393         ptrdiff_t idx = find(texture);
394         if (idx >= 0) 
395             return cast(uint)idx;
396 
397         uint id = cast(uint)textures.length;
398         textures ~= texture.retained;
399         return id;
400     }
401 
402     /**
403         Inserts a texture into the texture cache at the given index,
404         the texture cache is resized to accomodate the index.
405 
406         Params:
407             texture =   The texture to add to the cache.
408             at =        The index to give the texture.
409     */
410     void insert(Texture texture, size_t at) {
411         if (at >= textures.length)
412             textures.resize(at+1);
413 
414         textures[at] = texture.retained();
415     }
416 
417     /**
418         Prunes all textures from the cache, only leaving behind
419         textures referenced from outside of the cache.
420 
421         Any texture that is unused will be freed.
422     */
423     void prune() {
424         size_t alive = 0;
425         foreach (i; 0 .. textures.length) {
426             if (auto tex = textures[i].released()) {
427 
428                 // Avoid copy semantics, moving the alive texture
429                 // back to the lowest slot now available.
430                 // Then restore its refcount held by the cache.
431                 (cast(void*[])textures)[alive++] = cast(void*)tex;
432                 tex.retain();
433             }
434         }
435         textures.resize(alive);
436     }
437 
438     /**
439         Tries to get a texture from the cache.
440 
441         Params:
442             slotId = The texture slot ID to try to fetch.
443         
444         Returns:
445             The given texture if found, $(D null) otherwise.
446     */
447     Texture get(uint slotId) {
448         return slotId >= size ? null : textures[slotId];
449     }
450 
451     /**
452         Finds the slot of a given texture within the cache.
453 
454         Params:
455             texture = The texture to look for.
456         
457         Returns:
458             A non-negative number on success,
459             $(D -1) if the texture was not found.
460     */
461     ptrdiff_t find(Texture texture) {
462         foreach (i; 0 .. textures.length) {
463             if (textures[i] is texture)
464                 return i;
465         }
466         return -1;
467     }
468 }
469 
470 /**
471     Converts a Inochi2D TextureFormat to a Gamut PixelType.
472 
473     Params:
474         format = The texture format
475 
476     Returns:
477         The equivalent pixel type.    
478 */
479 PixelType toPixelType(TextureFormat format) @nogc nothrow pure {
480     final switch (format) {
481     case TextureFormat.none:
482         return PixelType.unknown;
483     case TextureFormat.r8:
484         return PixelType.l8;
485     case TextureFormat.rgba8Unorm:
486         return PixelType.rgba8;
487     }
488 }