1 /++
2  + Authors: Avaxar <avaxar@nekkl.org>
3  + Copyright: Copyright © 2023, Avaxar
4  + License: $(LINK2 https://mit-license.org, MIT License)
5  +/
6 
7 module dsdl2.texture;
8 @safe:
9 
10 import bindbc.sdl;
11 import dsdl2.sdl;
12 import dsdl2.blend;
13 import dsdl2.rect;
14 import dsdl2.frect;
15 import dsdl2.pixels;
16 import dsdl2.renderer;
17 import dsdl2.surface;
18 
19 import core.memory : GC;
20 import std.conv : to;
21 import std.format : format;
22 
23 /++
24  + D enum that wraps `SDL_TextureAccess` in specifying texture access mode
25  +/
26 enum TextureAccess {
27     /++
28      + Wraps `SDL_TEXTUREACCESS_*` enumeration constants
29      +/
30     static_ = SDL_TEXTUREACCESS_STATIC,
31     streaming = SDL_TEXTUREACCESS_STREAMING, /// ditto
32     target = SDL_TEXTUREACCESS_TARGET /// ditto
33 }
34 
35 static if (sdlSupport >= SDLSupport.v2_0_12) {
36     /++
37      + D enum that wraps `SDL_ScaleMode` in specifying mode of texture scaling
38      +/
39     enum ScaleMode {
40         /++
41          + Wraps `SDL_ScaleMode*` enumeration constants
42          +/
43         nearest = SDL_ScaleModeNearest,
44         linear = SDL_ScaleModeLinear, /// ditto
45         best = SDL_ScaleModeBest /// ditto
46     }
47 }
48 
49 /++
50  + D class that wraps `SDL_Texture` storing textures in the VRAM
51  +
52  + `dsdl2.Texture` stores a 2D image out of pixels with a `width` and `height`, where each pixel stored in the
53  + GPU RAM (VRAM) according to its defined `dsdl2.PixelFormat`. A `dsdl2.Texture` is associated with its
54  + `dsdl2.Renderer`, and can only be operated with/by it.
55  +
56  + Example:
57  + ---
58  + auto renderer = new dsdl2.Renderer(...);
59  + auto surface = new dsdl2.Surface(...);
60  +
61  + auto texture = new dsdl2.Texture(renderer, surface);
62  + ---
63  +/
64 final class Texture {
65     private PixelFormat pixelFormatProxy = null;
66     private bool isOwner = true;
67     private void* userRef = null;
68 
69     @system SDL_Texture* sdlTexture; /// Internal `SDL_Texture` pointer
70 
71     /++
72      + Constructs a `dsdl2.Texture` from a vanilla `SDL_Texture*` from bindbc-sdl
73      +
74      + Params:
75      +   sdlTexture = the `SDL_Texture` pointer to manage
76      +   isOwner = whether the instance owns the given `SDL_Texture*` and should destroy it on its own
77      +   userRef = optional pointer to maintain reference link, avoiding GC cleanup
78      +/
79     this(SDL_Texture* sdlTexture, bool isOwner = true, void* userRef = null) @system
80     in {
81         assert(sdlTexture !is null);
82     }
83     do {
84         this.sdlTexture = sdlTexture;
85         this.isOwner = isOwner;
86         this.userRef = userRef;
87     }
88 
89     /++
90      + Creates a blank `dsdl2.Texture` in the VRAM, which wraps `SDL_CreateTexture`
91      +
92      + Params:
93      +   renderer = `dsdl2.Renderer` the texture belongs to
94      +   pixelFormat = `dsdl2.PixelFormat` that the texture pixel data is stored as
95      +   access = `dsdl2.TextureAccess` enumeration which indicates its access rule
96      +   size = the size of the texture (width and height)
97      + Throws: `dsdl2.SDLException` if creation failed
98      +/
99     this(Renderer renderer, PixelFormat pixelFormat, TextureAccess access, uint[2] size) @trusted
100     in {
101         assert(renderer !is null);
102         assert(pixelFormat !is null);
103     }
104     do {
105         this.sdlTexture = SDL_CreateTexture(renderer.sdlRenderer, pixelFormat.sdlPixelFormatEnum, access,
106             size[0].to!int, size[1].to!int);
107         if (this.sdlTexture is null) {
108             throw new SDLException;
109         }
110     }
111 
112     /++
113      + Creates a `dsdl2.Texture` in the VRAM from a `dsdl2.Surface`, which wraps `SDL_CreateTextureFromSurface`
114      +
115      + Params:
116      +   renderer = `dsdl2.Renderer` the texture belongs to
117      +   surface = `dsdl2.Surface` for its pixel data to be copied over to the texture
118      + Throws: `dsdl2.SDLException` if creation failed
119      +/
120     this(Renderer renderer, Surface surface) @trusted
121     in {
122         assert(renderer !is null);
123         assert(surface !is null);
124     }
125     do {
126         this.sdlTexture = SDL_CreateTextureFromSurface(renderer.sdlRenderer, surface.sdlSurface);
127         if (this.sdlTexture is null) {
128             throw new SDLException;
129         }
130     }
131 
132     ~this() @trusted {
133         if (this.isOwner) {
134             SDL_DestroyTexture(this.sdlTexture);
135         }
136     }
137 
138     @trusted invariant { // @suppress(dscanner.trust_too_much)
139         // Instance might be in an invalid state due to holding a non-owned externally-freed object when
140         // destructed in an unpredictable order.
141         if (!this.isOwner && GC.inFinalizer) {
142             return;
143         }
144 
145         assert(this.sdlTexture !is null);
146     }
147 
148     /++
149      + Equality operator overload
150      +/
151     bool opEquals(const Texture rhs) const @trusted {
152         return this.sdlTexture is rhs.sdlTexture;
153     }
154 
155     /++
156      + Gets the hash of the `dsdl2.Texture`
157      +
158      + Returns: unique hash for the instance being the pointer of the internal `SDL_Texture` pointer
159      +/
160     override hash_t toHash() const @trusted {
161         return cast(hash_t) this.sdlTexture;
162     }
163 
164     /++
165      + Formats the `dsdl2.Texture` into its construction representation: `"dsdl2.Texture(<sdlTexture>)"`
166      +
167      + Returns: the formatted `string`
168      +/
169     override string toString() const @trusted {
170         return "dsdl2.Texture(0x%x)".format(this.sdlTexture);
171     }
172 
173     /++
174      + Wraps `SDL_QueryTexture` which gets the `dsdl2.PixelFormat` of the `dsdl2.Texture`
175      +
176      + Returns: `const` proxy to the `dsdl2.PixelFormat` of the `dsdl2.Texture`
177      + Throws: `dsdl2.SDLException` if failed to query the information
178      +/
179     const(PixelFormat) pixelFormat() const @property @trusted {
180         uint sdlPixelFormatEnum = void;
181         if (SDL_QueryTexture(cast(SDL_Texture*) this.sdlTexture, &sdlPixelFormatEnum, null, null, null) != 0) {
182             throw new SDLException;
183         }
184 
185         // If the actual pixel format happens to change, rewire the proxy.
186         if (this.pixelFormatProxy.sdlPixelFormatEnum != sdlPixelFormatEnum) {
187             (cast(Texture) this).pixelFormatProxy = new PixelFormat(sdlPixelFormatEnum);
188         }
189 
190         return this.pixelFormatProxy;
191     }
192 
193     /++
194      + Wraps `SDL_QueryTexture` which gets the `dsdl2.TextureAccess` of the `dsdl2.Texture`
195      +
196      + Returns: `dsdl2.TextureAccess` of the `dsdl2.Texture`
197      + Throws: `dsdl2.SDLException` if failed to query the information
198      +/
199     TextureAccess access() const @property @trusted {
200         TextureAccess texAccess = void;
201         if (SDL_QueryTexture(cast(SDL_Texture*) this.sdlTexture, null, cast(int*) texAccess, null, null) != 0) {
202             throw new SDLException;
203         }
204 
205         return texAccess;
206     }
207 
208     /++
209      + Wraps `SDL_QueryTexture` which gets the width of the `dsdl2.Texture` in pixels
210      +
211      + Returns: width of the `dsdl2.Texture` in pixels
212      + Throws: `dsdl2.SDLException` if failed to query the information
213      +/
214     uint width() const @property @trusted {
215         uint w = void;
216         if (SDL_QueryTexture(cast(SDL_Texture*) this.sdlTexture, null, null, cast(int*)&w, null) != 0) {
217             throw new SDLException;
218         }
219 
220         return w;
221     }
222 
223     /++
224      + Wraps `SDL_QueryTexture` which gets the height of the `dsdl2.Texture` in pixels
225      +
226      + Returns: height of the `dsdl2.Texture` in pixels
227      + Throws: `dsdl2.SDLException` if failed to query the information
228      +/
229     uint height() const @property @trusted {
230         uint h = void;
231         if (SDL_QueryTexture(cast(SDL_Texture*) this.sdlTexture, null, null, null, cast(int*)&h) != 0) {
232             throw new SDLException;
233         }
234 
235         return h;
236     }
237 
238     /++
239      + Wraps `SDL_QueryTexture` which gets the size of the `dsdl2.Texture` in pixels
240      +
241      + Returns: array of width and height of the `dsdl2.Texture` in pixels
242      + Throws: `dsdl2.SDLException` if failed to query the information
243      +/
244     uint[2] size() const @property @trusted {
245         uint[2] wh = void;
246         if (SDL_QueryTexture(cast(SDL_Texture*) this.sdlTexture, null, null, cast(int*)&wh[0],
247                 cast(int*)&wh[1]) != 0) {
248             throw new SDLException;
249         }
250 
251         return wh;
252     }
253 
254     /++
255      + Gets the color and alpha multipliers of the `dsdl2.Texture` that wraps `SDL_GetTextureColorMod` and
256      + `SDL_GetTextureAlphaMod`
257      +
258      + Returns: color and alpha multipliers of the `dsdl2.Texture`
259      +/
260     Color mod() const @property @trusted {
261         Color multipliers = void;
262         SDL_GetTextureColorMod(cast(SDL_Texture*) this.sdlTexture, &multipliers.sdlColor.r,
263             &multipliers.sdlColor.g, &multipliers.sdlColor.b);
264         SDL_GetTextureAlphaMod(cast(SDL_Texture*) this.sdlTexture, &multipliers.sdlColor.a);
265         return multipliers;
266     }
267 
268     /++
269      + Sets the color and alpha multipliers of the `dsdl2.Texture` that wraps `SDL_SetTextureColorMod` and
270      + `SDL_SetTextureAlphaMod`
271      +
272      + Params:
273      +   newMod = `dsdl2.Color` with `.r`, `.g`, `.b` as the color multipliers, and `.a` as the alpha multiplier
274      +/
275     void mod(Color newMod) @property @trusted {
276         SDL_SetTextureColorMod(this.sdlTexture, newMod.r, newMod.g, newMod.b);
277         SDL_SetTextureAlphaMod(this.sdlTexture, newMod.a);
278     }
279 
280     /++
281      + Wraps `SDL_GetTextureColorMod` which gets the color multipliers of the `dsdl2.Texture`
282      +
283      + Returns: color multipliers of the `dsdl2.Texture`
284      +/
285     ubyte[3] colorMod() const @property @trusted {
286         ubyte[3] rgbMod = void;
287         SDL_GetTextureColorMod(cast(SDL_Texture*) this.sdlTexture, &rgbMod[0], &rgbMod[1], &rgbMod[2]);
288         return rgbMod;
289     }
290 
291     /++
292      + Wraps `SDL_SetTextureColorMod` which sets the color multipliers of the `dsdl2.Texture`
293      +
294      + Params:
295      +   newColorMod = an array of `ubyte`s representing red, green, and blue multipliers (each 0-255)
296      +/
297     void colorMod(ubyte[3] newColorMod) @property @trusted {
298         SDL_SetTextureColorMod(this.sdlTexture, newColorMod[0], newColorMod[1], newColorMod[2]);
299     }
300 
301     /++
302      + Wraps `SDL_GetTextureAlphaMod` which gets the alpha multiplier of the `dsdl2.Texture`
303      +
304      + Returns: alpha multiplier of the `dsdl2.Texture`
305      +/
306     ubyte alphaMod() const @property @trusted {
307         ubyte aMod = void;
308         SDL_GetTextureAlphaMod(cast(SDL_Texture*) this.sdlTexture, &aMod);
309         return aMod;
310     }
311 
312     /++
313      + Wraps `SDL_SetTextureAlphaMod` which sets the alpha multiplier of the `dsdl2.Texture`
314      +
315      + Params:
316      +   newAlphaMod = alpha multiplier (0-255)
317      +/
318     void alphaMod(ubyte newAlphaMod) @property @trusted {
319         SDL_SetTextureAlphaMod(this.sdlTexture, newAlphaMod);
320     }
321 
322     /++
323      + Wraps `SDL_GetTextureBlendMode` which gets the `dsdl2.Texture`'s `dsdl2.BlendMode` defining drawing
324      +
325      + Returns: `dsdl2.BlendMode` of the `dsdl2.Texture`
326      + Throws: `dsdl2.SDLException` if `dsdl2.BlendMode` unable to get
327      +/
328     BlendMode blendMode() const @property @trusted {
329         BlendMode mode = void;
330         if (SDL_GetTextureBlendMode(cast(SDL_Texture*) this.sdlTexture, &mode.sdlBlendMode) != 0) {
331             throw new SDLException;
332         }
333 
334         return mode;
335     }
336 
337     /++
338      + Wraps `SDL_SetTextureBlendMode` which sets the `dsdl2.Texture`'s `dsdl2.BlendMode` defining drawing
339      +
340      + Params:
341      +   newMode = `dsdl2.BlendMode` to set
342      + Throws: `dsdl2.SDLException` if `dsdl2.BlendMode` unable to set
343      +/
344     void blendMode(BlendMode newMode) @property @trusted {
345         if (SDL_SetTextureBlendMode(this.sdlTexture, newMode.sdlBlendMode) != 0) {
346             throw new SDLException;
347         }
348     }
349 
350     /++
351      + Wraps `SDL_UpdateTexture` which updates the entire `dsdl2.Texture`'s pixel data
352      +
353      + Params:
354      +   pixels = array of pixels for the entire `dsdl2.Texture`'s pixels to be replaced with
355      +   pitch = skips in bytes per line/row of the `pixels`
356      + Throws: `dsdl2.SDLException` if failed to update the texture pixel data
357      +/
358     void update(const void[] pixels, size_t pitch) @trusted
359     in {
360         assert(pitch * 8 >= this.width * this.pixelFormat.bitsPerPixel);
361         assert(pixels.length == pitch * this.height);
362     }
363     do {
364         if (SDL_UpdateTexture(this.sdlTexture, null, pixels.ptr, pitch.to!int) != 0) {
365             throw new SDLException;
366         }
367     }
368 
369     /++
370      + Wraps `SDL_UpdateTexture` which updates the `dsdl2.Texture`'s pixel data at a certain `dsdl2.Rect` boundary
371      +
372      + Params:
373      +   rect = `dsdl2.Rect` boundary marking the part of the texture whose pixels are to be updated
374      +   pixels = array of pixels for the `dsdl2.Texture`'s `rect` pixels to be replaced with
375      +   pitch = skips in bytes per line/row of the `pixels`
376      + Throws: `dsdl2.SDLException` if failed to update the texture pixel data
377      +/
378     void update(Rect rect, void[] pixels, size_t pitch) @trusted
379     in {
380         assert(pitch * 8 >= rect.width * this.pixelFormat.bitsPerPixel);
381         assert(pixels.length == pitch * rect.height);
382     }
383     do {
384         if (SDL_UpdateTexture(this.sdlTexture, &rect.sdlRect, pixels.ptr, pitch.to!int) != 0) {
385             throw new SDLException;
386         }
387     }
388 
389     /++
390      + Wraps `SDL_GL_BindTexture` which binds the texture in OpenGL
391      +
392      + Returns: texture width and height in OpenGL
393      + Throws: `dsdl2.SDLException` if failed to bind
394      +/
395     float[2] bindGL() @trusted {
396         float[2] size = void;
397         if (SDL_GL_BindTexture(this.sdlTexture, &size[0], &size[1]) != 0) {
398             throw new SDLException;
399         }
400 
401         return size;
402     }
403 
404     /++
405      + Wraps `SDL_GL_UnbindTexture` which unbinds the texture in OpenGL
406      +
407      + Throws: `dsdl2.SDLException` if failed to unbind
408      +/
409     void unbindGL() @trusted {
410         if (SDL_GL_UnbindTexture(this.sdlTexture) != 0) {
411             throw new SDLException;
412         }
413     }
414 
415     static if (sdlSupport >= SDLSupport.v2_0_12) {
416         /++
417          + Wraps `SDL_GetTextureScaleMode` (from SDL 2.0.12) which gets the texture's scaling mode
418          +
419          + Returns: `dsdl2.ScaleMode` of the texture
420          +/
421         ScaleMode scaleMode() const @property @trusted
422         in {
423             assert(getVersion() >= Version(2, 0, 12));
424         }
425         do {
426             SDL_ScaleMode sdlScaleMode = void;
427             if (SDL_GetTextureScaleMode(cast(SDL_Texture*) this.sdlTexture, &sdlScaleMode) != 0) {
428                 throw new SDLException;
429             }
430 
431             return cast(ScaleMode) sdlScaleMode;
432         }
433 
434         /++
435          + Wraps `SDL_SetTextureScaleMode` (from SDL 2.0.12) which sets the texture's scaling mode
436          +
437          + Params:
438          +   newMode = new `dsdl2.ScaleMode` set
439          +/
440         void scaleMode(ScaleMode newMode) @property @trusted
441         in {
442             assert(getVersion() >= Version(2, 0, 12));
443         }
444         do {
445             if (SDL_SetTextureScaleMode(this.sdlTexture, cast(SDL_ScaleMode) newMode) != 0) {
446                 throw new SDLException;
447             }
448         }
449     }
450 }