vsync is now default mode
[dd2d.git] / glutils.d
blob7cdd5fb4ec56cd36322cd0edd9a0406e1b7ccbf4
1 /* DooM2D: Midnight on the Firing Line
2 * coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
3 * Understanding is not required. Only obedience.
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 module glutils;
19 private:
20 import iv.glbinds;
21 import arsd.color;
22 import arsd.png;
24 import wadarc;
27 // ////////////////////////////////////////////////////////////////////////// //
28 __gshared bool glutilsShowShaderWarnings = false; // shut up!
31 __gshared GLuint glLastUsedTexture = 0;
33 public void useTexture (GLuint tid) {
34 pragma(inline, true);
35 if (glLastUsedTexture != tid) {
36 glLastUsedTexture = tid;
37 glBindTexture(GL_TEXTURE_2D, tid);
41 public void useTexture (Texture tex) { pragma(inline, true); useTexture(tex !is null ? tex.tid : 0); }
45 public TrueColorImage loadPngFile (string fname) {
46 auto fl = openFile(fname);
47 auto sz = fl.size;
48 if (sz < 4 || sz > 32*1024*1024) throw new Exception("invalid png file size: '"~fname~"'");
49 if (sz == 0) return null;
50 auto res = new ubyte[](cast(uint)sz);
51 if (fl.rawRead(res[]).length != res.length) throw new Exception("error reading png file '"~fname~"'");
52 return imageFromPng(readPng(res)).getAsTrueColorImage;
56 // ////////////////////////////////////////////////////////////////////////// //
57 public final class Texture {
58 GLuint tid;
59 int width, height;
61 // default: repeat, linear
62 enum Option : int {
63 Repeat,
64 Clamp,
65 ClampBorder,
66 Linear,
67 Nearest,
68 UByte,
69 Float, // create floating point texture
72 this (string fname, in Option[] opts...) { loadPng(fname, opts); }
73 this (int w, int h, in Option[] opts...) { createIntr(w, h, null, opts); }
74 this (TrueColorImage aimg, Option[] opts...) { createIntr(aimg.width, aimg.height, aimg, opts); }
75 ~this () { clear(); }
78 void clear () {
79 if (tid) {
80 //useTexture(tid);
81 glBindTexture(GL_TEXTURE_2D, tid);
82 glDeleteTextures(1, &tid);
83 //useTexture(0);
84 glBindTexture(GL_TEXTURE_2D, 0);
85 tid = 0;
86 width = 0;
87 height = 0;
91 private static void processOpt (GLuint* wrapOpt, GLuint* filterOpt, GLuint* ttype, in Option[] opts...) {
92 foreach (immutable opt; opts) {
93 switch (opt) with (Option) {
94 case Repeat: *wrapOpt = GL_REPEAT; break;
95 case Clamp: *wrapOpt = GL_CLAMP_TO_EDGE; break;
96 case ClampBorder: *wrapOpt = GL_CLAMP_TO_BORDER; break;
97 case Linear: *filterOpt = GL_LINEAR; break;
98 case Nearest: *filterOpt = GL_NEAREST; break;
99 case UByte: *ttype = GL_UNSIGNED_BYTE; break;
100 case Float: *ttype = GL_FLOAT; break;
101 default:
106 void createIntr (int w, int h, TrueColorImage img, in Option[] opts...) {
107 import core.stdc.stdlib : malloc, free;
108 assert(w > 0);
109 assert(h > 0);
110 clear();
112 GLuint wrapOpt = GL_REPEAT;
113 GLuint filterOpt = GL_LINEAR;
114 GLuint ttype = GL_UNSIGNED_BYTE;
115 processOpt(&wrapOpt, &filterOpt, &ttype, opts);
117 glGenTextures(1, &tid);
118 //useTexture(tid);
119 glBindTexture(GL_TEXTURE_2D, tid);
120 scope(exit) glBindTexture(GL_TEXTURE_2D, 0);
121 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapOpt);
122 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapOpt);
123 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filterOpt);
124 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filterOpt);
125 //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
126 //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
127 GLfloat[4] bclr = 0.0;
128 glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, bclr.ptr);
129 if (img !is null && img.width == w && img.height == h) {
130 // create straight from image
131 glTexImage2D(GL_TEXTURE_2D, 0, (ttype == GL_FLOAT ? GL_RGBA16F : GL_RGBA), w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, img.imageData.bytes.ptr);
132 } else {
133 // create empty texture
134 ubyte* ptr = null;
135 scope(exit) if (ptr !is null) free(ptr);
137 import core.stdc.string : memset;
138 ptr = cast(ubyte*)malloc(w*h*4);
139 if (ptr !is null) memset(ptr, 0, w*h*4);
141 glTexImage2D(GL_TEXTURE_2D, 0, (ttype == GL_FLOAT ? GL_RGBA16F : GL_RGBA), w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, ptr);
142 if (img !is null && img.width > 0 && img.height > 0) {
143 // setup from image
144 //TODO: dunno if it's ok to use images bigger than texture here
145 glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, img.width, img.height, GL_RGBA, GL_UNSIGNED_BYTE, img.imageData.bytes.ptr);
146 // the following is ok too
147 //glBindTexture(GL_TEXTURE_2D, 0);
148 //glTextureSubImage2D(tid, 0, 0, 0, img.width, img.height, GL_RGBA, GL_UNSIGNED_BYTE, img.imageData.bytes.ptr);
151 width = w;
152 height = h;
155 void setFromImage (TrueColorImage img, int x, int y) {
156 if (img is null || !tid || img.height < 1 || img.width < 1) return;
157 if (x >= width || y >= height) return;
158 if (x+img.width <= 0 || y+img.height <= 0) return; //TODO: overflow
159 if (x >= 0 && y >= 0 && x+img.width <= width && y+img.height <= height) {
160 // easy case, just copy it
161 glTextureSubImage2D(tid, 0, x, y, img.width, img.height, GL_RGBA, GL_UNSIGNED_BYTE, img.imageData.bytes.ptr);
162 } else {
163 import core.stdc.stdlib : malloc, free;
164 import core.stdc.string : memset, memcpy;
165 // hard case, have to build the temp region
166 uint* src = cast(uint*)img.imageData.bytes.ptr;
167 // calc x skip and effective width
168 int rwdt = img.width;
169 if (x < 0) {
170 rwdt += x;
171 src -= x; // as `x` is negative here
172 x = 0;
174 if (x+rwdt > width) rwdt = width-x;
175 // calc y skip and effective height
176 int rhgt = img.height;
177 if (y < 0) {
178 rhgt += y;
179 src -= y*img.width; // as `y` is negative here
180 y = 0;
182 if (y+rhgt > height) rhgt = height-y;
183 assert(rwdt > 0 && rhgt > 0);
184 uint* ptr = null;
185 scope(exit) if (ptr !is null) free(ptr);
186 ptr = cast(uint*)malloc(rwdt*rhgt*4);
187 if (ptr is null) assert(0, "out of memory in `Texture.setFromImage()`");
188 // now copy
189 auto d = ptr;
190 foreach (immutable _; 0..rhgt) {
191 memcpy(d, src, rwdt*4);
192 src += img.width;
193 d += rwdt;
195 glTextureSubImage2D(tid, 0, x, y, rwdt, rhgt, GL_RGBA, GL_UNSIGNED_BYTE, ptr);
199 void loadPng (string fname, in Option[] opts...) {
200 scope(failure) clear;
201 auto img = loadPngFile(fname);
202 createIntr(img.width, img.height, img, opts);
205 void activate () { if (tid) glBindTexture(GL_TEXTURE_2D, tid); }
206 void deactivate () { if (tid) glBindTexture(GL_TEXTURE_2D, 0); }
210 // ////////////////////////////////////////////////////////////////////////// //
212 public final class TextureCube {
213 GLuint tid;
214 int width, height;
216 this (string fname, in Texture.Option[] opts...) { loadPng(fname, opts); }
217 this (int w, int h, in Texture.Option[] opts...) { createIntr(w, h, opts); }
218 ~this () { clear(); }
221 void clear () {
222 if (tid) {
223 glBindTexture(GL_TEXTURE_CUBE_MAP, tid);
224 glDeleteTextures(1, &tid);
225 glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
226 tid = 0;
227 width = 0;
228 height = 0;
232 void createIntr (int w, int h, in Texture.Option[] opts...) {
233 import core.stdc.stdlib : malloc, free;
234 assert(w > 0);
235 assert(h > 0);
237 GLuint wrapOpt = GL_CLAMP_TO_EDGE;
238 GLuint filterOpt = GL_LINEAR;
239 GLuint ttype = GL_FLOAT;
240 Texture.processOpt(&wrapOpt, &filterOpt, &ttype, opts);
242 glGenTextures(1, &tid);
243 glBindTexture(GL_TEXTURE_CUBE_MAP, tid);
244 scope(exit) glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
245 glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, wrapOpt);
246 glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, wrapOpt);
247 glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, wrapOpt);
248 glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, filterOpt);
249 glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, filterOpt);
250 GLfloat[4] bclr = 0.0;
251 glTexParameterfv(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_BORDER_COLOR, bclr.ptr);
253 ubyte* ptr = null;
254 scope(exit) if (ptr !is null) free(ptr);
256 import core.stdc.string : memset;
257 ptr = cast(ubyte*)malloc(w*h*4*4);
258 memset(ptr, 0, w*h*4);
260 foreach (int idx; 0..6) {
261 glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X+idx, 0, (ttype == GL_FLOAT ? GL_RGBA16F : GL_RGBA), w, h, 0, GL_BGRA, GL_FLOAT, ptr);
263 width = w;
264 height = h;
267 void loadPng (string fname, in Texture.Option[] opts...) {
268 clear();
269 TrueColorImage[6] img;
270 foreach (int idx; 0..6) {
271 import std.string : format;
272 img[idx] = loadPngFile(fname.format(idx));
273 if (idx > 0) {
274 if (img[idx].width != img[idx-1].width || img[idx].height != img[idx-1].height) {
275 assert(0, "cubemap fucked: "~fname);
279 createIntr(img[0].width, img[0].height, opts);
280 glBindTexture(GL_TEXTURE_CUBE_MAP, tid);
281 scope(exit) glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
282 foreach (int idx; 0..6) {
283 glTexSubImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X+idx, 0, 0, 0, img[idx].width, img[idx].height, GL_RGBA, GL_UNSIGNED_BYTE, img[idx].imageData.bytes.ptr);
287 void activate () { if (tid) glBindTexture(GL_TEXTURE_CUBE_MAP, tid); }
288 void deactivate () { if (tid) glBindTexture(GL_TEXTURE_CUBE_MAP, 0); }
293 // ////////////////////////////////////////////////////////////////////////// //
294 public final class FBO {
295 int width;
296 int height;
297 GLuint fbo;
298 Texture tex;
300 this (Texture atex) { createWithTexture(atex); }
302 this (int wdt, int hgt, Texture.Option[] opts...) {
303 createWithTexture(new Texture(wdt, hgt, opts));
306 ~this () {
307 //FIXME: this may be wrong, as texture may be already destroyed (and it's wrong too); we need refcount for textures
308 glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, 0, 0);
309 glDeleteFramebuffersEXT(1, &fbo);
310 fbo = 0;
313 void clear () {
314 if (fbo) {
315 // detach texture
316 glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, 0, 0);
317 glDeleteFramebuffersEXT(1, &fbo);
318 fbo = 0;
320 if (tex !is null) tex.clear();
321 tex = null;
324 void activate () { if (fbo) glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fbo); }
325 void deactivate () { glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0); }
327 // this will deactivate current FBO!
328 void replaceTexture (Texture ntex) {
329 if (tex !is null) {
330 if (ntex !is null && ntex.tid == tex.tid) return;
331 } else {
332 if (ntex is null) return;
334 glGenFramebuffersEXT(1, &fbo);
335 scope(exit) glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
336 // detach texture
337 glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, 0, 0);
338 glDeleteFramebuffersEXT(1, &fbo);
339 fbo = 0;
340 tex = ntex;
341 // attach texture
342 if (tex !is null) {
343 glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, tex.tid, 0);
345 GLenum status = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT);
346 if (status != GL_FRAMEBUFFER_COMPLETE_EXT) assert(0, "framebuffer fucked!");
351 private:
352 void createWithTexture (Texture atex) {
353 assert(atex !is null && atex.tid);
355 tex = atex;
356 glGenFramebuffersEXT(1, &fbo);
357 glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fbo);
358 scope(exit) glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
359 // attach 2D texture to this FBO
360 glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, tex.tid, 0);
361 // GL_COLOR_ATTACHMENT0_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_STENCIL_ATTACHMENT_EXT
364 glGenRenderbuffersEXT(1, &fboDepthId);
365 glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, fboDepthId);
366 glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, GL_DEPTH_COMPONENT24, LightSize, LightSize);
367 // attach depth buffer to FBO
368 glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT, fboDepthId);
369 // kill it
370 glDeleteRenderbuffersEXT(1, &fboDepthId);
374 GLenum status = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT);
375 if (status != GL_FRAMEBUFFER_COMPLETE_EXT) assert(0, "framebuffer fucked!");
377 glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
379 width = tex.width;
380 height = tex.height;
385 // ////////////////////////////////////////////////////////////////////////// //
386 public struct SVec2I { int x, y; }
387 public struct SVec3I { int x, y, z; }
388 public struct SVec4I { int x, y, z, w; }
390 public struct SVec2F { float x, y; }
391 public struct SVec3F { float x, y, z; }
392 public struct SVec4F { float x, y, z, w; }
395 public final class Shader {
396 string shaderName;
397 GLuint prg = 0;
398 GLint[string] vars;
400 this (string ashaderName, const(char)[] src) {
401 shaderName = ashaderName;
402 if (src.length > int.max) {
403 import core.stdc.stdio : printf;
404 printf("shader '%.*s' code too long!", cast(uint)ashaderName.length, ashaderName.ptr);
405 assert(0);
407 auto shaderId = glCreateShader(GL_FRAGMENT_SHADER);
408 auto sptr = src.ptr;
409 GLint slen = cast(int)src.length;
410 glShaderSource(shaderId, 1, &sptr, &slen);
411 glCompileShader(shaderId);
412 GLint success = 0;
413 glGetShaderiv(shaderId, GL_COMPILE_STATUS, &success);
414 if (!success || glutilsShowShaderWarnings) {
415 import core.stdc.stdio : printf;
416 import core.stdc.stdlib : malloc, free;
417 GLint logSize = 0;
418 glGetShaderiv(shaderId, GL_INFO_LOG_LENGTH, &logSize);
419 if (logSize > 0) {
420 auto logStrZ = cast(GLchar*)malloc(logSize);
421 glGetShaderInfoLog(shaderId, logSize, null, logStrZ);
422 printf("shader '%.*s' compilation messages:\n%s\n", cast(uint)ashaderName.length, ashaderName.ptr, logStrZ);
423 free(logStrZ);
426 if (!success) assert(0);
427 prg = glCreateProgram();
428 glAttachShader(prg, shaderId);
429 glLinkProgram(prg);
432 GLint varId(NT) (NT vname) if (is(NT == char[]) || is(NT == const(char)[]) || is(NT == immutable(char)[])) {
433 GLint id = -1;
434 if (vname.length > 0 && vname.length <= 128) {
435 if (auto vi = vname in vars) {
436 id = *vi;
437 } else {
438 char[129] buf = void;
439 buf[0..vname.length] = vname[];
440 buf[vname.length] = 0;
441 id = glGetUniformLocation(prg, buf.ptr);
442 //{ import core.stdc.stdio; printf("[%.*s.%s]=%i\n", cast(uint)shaderName.length, shaderName.ptr, buf.ptr, id); }
443 static if (is(NT == immutable(char)[])) {
444 vars[vname.idup] = id;
445 } else {
446 vars[vname.idup] = id;
448 if (id < 0) {
449 import core.stdc.stdio : printf;
450 printf("shader '%.*s': unknown variable '%.*s'\n", cast(uint)shaderName.length, shaderName.ptr, cast(uint)vname.length, vname.ptr);
454 return id;
457 // get unified var id
458 GLint opIndex(NT) (NT vname) if (is(NT == char[]) || is(NT == const(char)[]) || is(NT == immutable(char)[])) {
459 auto id = varId(vname);
460 if (id < 0) {
461 import core.stdc.stdio : printf;
462 printf("shader '%.*s': unknown variable '%.*s'\n", cast(uint)shaderName.length, shaderName.ptr, cast(uint)vname.length, vname.ptr);
463 assert(0);
465 return id;
468 private import std.traits;
469 void opIndexAssign(T, NT) (in auto ref T v, NT vname)
470 if (((isIntegral!T && T.sizeof <= 4) || (isFloatingPoint!T && T.sizeof == float.sizeof) || isBoolean!T ||
471 is(T : SVec2I) || is(T : SVec3I) || is(T : SVec4I) ||
472 is(T : SVec2F) || is(T : SVec3F) || is(T : SVec4F)) &&
473 (is(NT == char[]) || is(NT == const(char)[]) || is(NT == immutable(char)[])))
475 auto id = varId(vname);
476 if (id < 0) return;
477 //{ import core.stdc.stdio; printf("setting '%.*s' (%d)\n", cast(uint)vname.length, vname.ptr, id); }
478 static if (isIntegral!T || isBoolean!T) glUniform1i(id, cast(int)v);
479 else static if (isFloatingPoint!T) glUniform1f(id, cast(float)v);
480 else static if (is(SVec2I : T)) glUniform2i(id, cast(int)v.x, cast(int)v.y);
481 else static if (is(SVec3I : T)) glUniform3i(id, cast(int)v.x, cast(int)v.y, cast(int)v.z);
482 else static if (is(SVec4I : T)) glUniform4i(id, cast(int)v.x, cast(int)v.y, cast(int)v.z, cast(int)v.w);
483 else static if (is(SVec2F : T)) glUniform2f(id, cast(float)v.x, cast(float)v.y);
484 else static if (is(SVec3F : T)) glUniform3f(id, cast(float)v.x, cast(float)v.y, cast(float)v.z);
485 else static if (is(SVec4F : T)) glUniform4f(id, cast(float)v.x, cast(float)v.y, cast(float)v.z, cast(float)v.w);
486 else static assert(0, "wtf?!");
489 void activate () { if (prg) glUseProgram(prg); }
490 void deactivate () { glUseProgram(0); }
494 // ////////////////////////////////////////////////////////////////////////// //
495 //private import std.traits;
497 public:
498 void exec(TO) (TO obj, scope void delegate () dg) if (is(typeof(() { obj.activate(); obj.deactivate(); }))) {
499 obj.activate();
500 scope(exit) obj.deactivate();
501 dg();
504 void exec(TO, TG) (TO obj, scope TG dg) if (is(typeof((TO obj) { dg(obj); })) && is(typeof(() { obj.activate(); obj.deactivate(); }))) {
505 obj.activate();
506 scope(exit) obj.deactivate();
507 dg(obj);
511 // ////////////////////////////////////////////////////////////////////////// //
512 void orthoCamera (int wdt, int hgt) {
513 glMatrixMode(GL_PROJECTION); // for ortho camera
514 glLoadIdentity();
515 // left, right, bottom, top, near, far
516 //glOrtho(0, wdt, 0, hgt, -1, 1); // bottom-to-top
517 glOrtho(0, wdt, hgt, 0, -1, 1); // top-to-bottom
518 glViewport(0, 0, wdt, hgt);
520 //glTranslatef(-cx, -cy, 0.0f);
523 // origin is texture left top
524 void drawAtXY (GLuint tid, int x, int y, int w, int h, bool mirrorX=false, bool mirrorY=false) {
525 if (!tid || w < 1 || h < 1) return;
526 w += x;
527 h += y;
528 if (mirrorX) { int tmp = x; x = w; w = tmp; }
529 if (mirrorY) { int tmp = y; y = h; h = tmp; }
530 glBindTexture(GL_TEXTURE_2D, tid);
531 glBegin(GL_QUADS);
532 glTexCoord2f(0.0f, 0.0f); glVertex2i(x, y); // top-left
533 glTexCoord2f(1.0f, 0.0f); glVertex2i(w, y); // top-right
534 glTexCoord2f(1.0f, 1.0f); glVertex2i(w, h); // bottom-right
535 glTexCoord2f(0.0f, 1.0f); glVertex2i(x, h); // bottom-left
536 glEnd();
540 // origin is texture center
541 void drawAtXYC (Texture tex, int x, int y, bool mirrorX=false, bool mirrorY=false) {
542 if (tex is null || !tex.tid) return;
543 x -= tex.width/2;
544 y -= tex.height/2;
545 drawAtXY(tex.tid, x, y, tex.width, tex.height, mirrorX, mirrorY);
549 // origin is texture left top
550 void drawAtXY (Texture tex, int x, int y, bool mirrorX=false, bool mirrorY=false) {
551 if (tex is null || !tex.tid) return;
552 drawAtXY(tex.tid, x, y, tex.width, tex.height, mirrorX, mirrorY);