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 }