master uint/magnolia / source / renderer / renderer_small3dlib.c
  1#include <stdlib.h>
  2#include <string.h>
  3#include <stb_image_c89.h>
  4
  5#ifdef MG_PLATFORM_SDL
  6	#include <SDL2/SDL.h>
  7#endif
  8
  9#include "util.h"
 10#include "types.h"
 11#include "platform/platform.h"
 12#include "renderer/renderer.h"
 13
 14#define MAX_MESHES (16384)
 15#define MAX_TEXTURES (512)
 16#define S3L_MAX_WIDTH (800)
 17#define S3L_MAX_HEIGHT (600)
 18#define SOFTWARE_MAX_WIDTH (800)
 19#define SOFTWARE_MAX_HEIGHT (600)
 20#define TEXTURE_SAMPLE_PERIOD (4)
 21
 22#define S3L_MAX_PIXELS (S3L_MAX_WIDTH * S3L_MAX_HEIGHT)
 23#define S3L_PERSPECTIVE_CORRECTION 2
 24#define S3L_PC_APPROX_LENGTH 32
 25#define S3L_NEAR_CROSS_STRATEGY 1
 26#define S3L_Z_BUFFER 1
 27#define S3L_PIXEL_FUNCTION small3dlib_pixel
 28#include <small3dlib.h>
 29
 30typedef struct {
 31	float x;
 32	float y;
 33	float z;
 34	float w;
 35} ClipVertex;
 36
 37typedef struct {
 38	MGVertex* vtc; /* vertices */
 39	u16*      idc; /* indices */
 40	u32       vc; /* vertex count */
 41	u32       ic; /* index count */
 42	i8        used;
 43} RendererMesh;
 44
 45typedef struct {
 46	u32* pixels;
 47	i32  width;
 48	i32  height;
 49	i8   used;
 50} RendererTexture;
 51
 52typedef struct {
 53	MGContext* ctx;
 54	i8         pass; /* basically a bool; in
 55	            render pass or not */
 56
 57#ifdef MG_PLATFORM_SDL
 58	SDL_Texture* screen_texture;
 59#endif
 60	u16* framebuffer;
 61	i32  width;
 62	i32  height;
 63
 64	RendererMesh    meshes[MAX_MESHES];
 65	RendererTexture textures[MAX_TEXTURES];
 66
 67	RendererMesh*    active_mesh;
 68	RendererTexture* active_texture;
 69	u32              active_triangle;
 70	i32              tri_uvs[6];
 71	u32              sampled_colour;
 72	u8               texture_sample_count;
 73
 74	ClipVertex* clip_vertices;
 75	S3L_Vec4*   screen_vertices;
 76	u32         scratch_capacity;
 77} RendererState;
 78
 79static RendererState ren;
 80
 81/* create or resize the SDL texture used to present the software framebuffer */
 82static void create_screen_texture(i32 width, i32 height);
 83
 84/* destroy the SDL texture and software framebuffer */
 85static void destroy_screen_texture(void);
 86
 87static ClipVertex transform_vertex(const MGMat4* mvp, const MGVertex* v);
 88static i8         triangle_outside(const ClipVertex* a, const ClipVertex* b, const ClipVertex* c);
 89static i8         triangle_front_facing(const ClipVertex* a, const ClipVertex* b, const ClipVertex* c);
 90static S3L_Vec4   clip_to_screen(const ClipVertex* v);
 91static u32        sample_texture(const RendererMesh* mesh, const RendererTexture* texture, const S3L_PixelInfo* pixel);
 92static u16        rgba_to_rgb565(u32 rgba);
 93#ifndef MG_PLATFORM_SDL
 94static u32 rgb565_to_argb(u16 rgb);
 95#endif
 96static u32         pack_texture_pixel(u32 rgba);
 97static inline void small3dlib_pixel(S3L_PixelInfo* pixel);
 98
 99/* shade small3dlib pixel */
100static inline void small3dlib_pixel(S3L_PixelInfo* pixel)
101{
102	u32 index;
103	u32 sampled;
104
105	if (!ren.active_mesh || !ren.active_texture || !ren.framebuffer)
106		return;
107
108	if (ren.active_triangle != (u32)pixel->triangleIndex)
109		ren.texture_sample_count = 0;
110	if (ren.texture_sample_count == 0) {
111		ren.sampled_colour = sample_texture(ren.active_mesh, ren.active_texture, pixel);
112		if ((ren.sampled_colour >> 24) == 0xff)
113			ren.texture_sample_count = TEXTURE_SAMPLE_PERIOD;
114	}
115	sampled = ren.sampled_colour;
116	if (ren.texture_sample_count)
117		--ren.texture_sample_count;
118	if ((sampled >> 24) < 128) {
119		S3L_zBufferWrite(pixel->x, pixel->y, pixel->previousZ);
120		return;
121	}
122	index = (u32)pixel->y * (u32)ren.width + (u32)pixel->x;
123	ren.framebuffer[index] = (u16)sampled;
124}
125
126/* create or resize screen texture */
127static void create_screen_texture(i32 width, i32 height)
128{
129	u64  pixel_count;
130	u16* framebuffer;
131
132#ifdef MG_PLATFORM_SDL
133	if (width == ren.width && height == ren.height && ren.framebuffer && ren.screen_texture)
134		return;
135#else
136	if (width == ren.width && height == ren.height && ren.framebuffer)
137		return;
138#endif
139
140	if (width <= 0 || height <= 0)
141		return;
142
143	if (width > S3L_MAX_WIDTH || height > S3L_MAX_HEIGHT)
144		mg_die(MG_EC_SMALL3D_FRAMEBUFFER_LIMIT, "small3dlib framebuffer exceeds %dx%d", S3L_MAX_WIDTH,
145		       S3L_MAX_HEIGHT);
146
147	pixel_count = (u64)width * (u64)height;
148	framebuffer = malloc(pixel_count * sizeof(*framebuffer));
149	if (!framebuffer)
150		mg_die(MG_EC_SMALL3D_FRAMEBUFFER_ALLOC, "Failed to allocate small3dlib framebuffer");
151
152#ifdef MG_PLATFORM_SDL
153	if (ren.screen_texture)
154		SDL_DestroyTexture(ren.screen_texture);
155
156	ren.screen_texture = SDL_CreateTexture(ren.ctx->vbe.renderer, SDL_PIXELFORMAT_RGB565,
157	                                       SDL_TEXTUREACCESS_STREAMING, width, height);
158
159	if (!ren.screen_texture)
160		mg_die(MG_EC_SMALL3D_SCREEN_TEXTURE_CREATE, "Failed to create small3dlib texture: %s", SDL_GetError());
161#endif
162
163	free(ren.framebuffer);
164	ren.framebuffer = framebuffer;
165	ren.width = width;
166	ren.height = height;
167
168	S3L_resolutionX = (uint16_t)width;
169	S3L_resolutionY = (uint16_t)height;
170}
171
172/* destroy screen texture */
173static void destroy_screen_texture(void)
174{
175#ifdef MG_PLATFORM_SDL
176	if (ren.screen_texture) {
177		SDL_DestroyTexture(ren.screen_texture);
178		ren.screen_texture = NULL;
179	}
180#endif
181
182	free(ren.framebuffer);
183	ren.framebuffer = NULL;
184	ren.width = 0;
185	ren.height = 0;
186}
187
188/* transform vertex to clip space
189   returns clip space vertex */
190static ClipVertex transform_vertex(const MGMat4* mvp, const MGVertex* v)
191{
192	ClipVertex r;
193
194	r.x =
195	    mvp->Elements[0][0] * v->x + mvp->Elements[1][0] * v->y + mvp->Elements[2][0] * v->z + mvp->Elements[3][0];
196	r.y =
197	    mvp->Elements[0][1] * v->x + mvp->Elements[1][1] * v->y + mvp->Elements[2][1] * v->z + mvp->Elements[3][1];
198	r.z =
199	    mvp->Elements[0][2] * v->x + mvp->Elements[1][2] * v->y + mvp->Elements[2][2] * v->z + mvp->Elements[3][2];
200	r.w =
201	    mvp->Elements[0][3] * v->x + mvp->Elements[1][3] * v->y + mvp->Elements[2][3] * v->z + mvp->Elements[3][3];
202
203	return r;
204}
205
206/* test whether triangle is outside clip volume
207   returns 1 if outside, 0 otherwise */
208static i8 triangle_outside(const ClipVertex* a, const ClipVertex* b, const ClipVertex* c)
209{
210	if (a->w <= 0.0001f || b->w <= 0.0001f || c->w <= 0.0001f)
211		return 1;
212
213	if (a->x < -a->w && b->x < -b->w && c->x < -c->w)
214		return 1;
215	if (a->x > a->w && b->x > b->w && c->x > c->w)
216		return 1;
217	if (a->y < -a->w && b->y < -b->w && c->y < -c->w)
218		return 1;
219	if (a->y > a->w && b->y > b->w && c->y > c->w)
220		return 1;
221	if (a->z < -a->w || b->z < -b->w || c->z < -c->w)
222		return 1;
223	if (a->z > a->w && b->z > b->w && c->z > c->w)
224		return 1;
225
226	return 0;
227}
228
229/* test whether triangle faces camera
230   returns 1 if front facing, 0 otherwise */
231static i8 triangle_front_facing(const ClipVertex* a, const ClipVertex* b, const ClipVertex* c)
232{
233	float ax = a->x / a->w;
234	float ay = a->y / a->w;
235	float bx = b->x / b->w;
236	float by = b->y / b->w;
237	float cx = c->x / c->w;
238	float cy = c->y / c->w;
239	float area = (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
240
241	return area > 0.0f;
242}
243
244/* convert clip vertex to screen coordinates
245   returns small3dlib screen vertex */
246static S3L_Vec4 clip_to_screen(const ClipVertex* v)
247{
248	S3L_Vec4 r;
249	float    inv_w = 1.0f / v->w;
250	float    ndc_x = v->x * inv_w;
251	float    ndc_y = v->y * inv_w;
252
253	r.x = (S3L_Unit)((ndc_x * 0.5f + 0.5f) * (float)(ren.width - 1));
254	r.y = (S3L_Unit)((0.5f - ndc_y * 0.5f) * (float)(ren.height - 1));
255	/* keep enough reciprocal depth precision for a
256	   useful amount of perspective correction */
257	r.z = (S3L_Unit)(v->w * 16.0f);
258	if (r.z < 1)
259		r.z = 1;
260	r.w = S3L_F;
261
262	return r;
263}
264
265/* sample texture for rasterized pixel
266   returns packed sampled colour */
267static u32 sample_texture(const RendererMesh* mesh, const RendererTexture* texture, const S3L_PixelInfo* pixel)
268{
269	u32             sampled;
270	u32             colour;
271	u32             r;
272	u32             g;
273	u32             b;
274	u32             tri;
275	const MGVertex* v[3];
276	i32             x;
277	i32             y;
278	i32             weighted_x;
279	i32             weighted_y;
280
281	if (!texture || !texture->pixels)
282		return 0xffffffff;
283
284	if (ren.active_triangle != (u32)pixel->triangleIndex) {
285		ren.active_triangle = pixel->triangleIndex;
286		tri = (u32)pixel->triangleIndex * 3;
287		v[0] = &mesh->vtc[mesh->idc[tri + 0]];
288		v[1] = &mesh->vtc[mesh->idc[tri + 1]];
289		v[2] = &mesh->vtc[mesh->idc[tri + 2]];
290
291		ren.tri_uvs[0] = (i32)(v[0]->u * (float)texture->width);
292		ren.tri_uvs[1] = (i32)(v[0]->v * (float)texture->height);
293		ren.tri_uvs[2] = (i32)(v[1]->u * (float)texture->width);
294		ren.tri_uvs[3] = (i32)(v[1]->v * (float)texture->height);
295		ren.tri_uvs[4] = (i32)(v[2]->u * (float)texture->width);
296		ren.tri_uvs[5] = (i32)(v[2]->v * (float)texture->height);
297	}
298
299	weighted_x = pixel->barycentric[0] * ren.tri_uvs[0] + pixel->barycentric[1] * ren.tri_uvs[2] +
300	             pixel->barycentric[2] * ren.tri_uvs[4];
301	weighted_y = pixel->barycentric[0] * ren.tri_uvs[1] + pixel->barycentric[1] * ren.tri_uvs[3] +
302	             pixel->barycentric[2] * ren.tri_uvs[5];
303	x = weighted_x >> 9;
304	y = weighted_y >> 9;
305	if (x < 0) {
306		x = 0;
307	}
308	else if (x >= texture->width) {
309		x = texture->width - 1;
310	}
311	if (y < 0) {
312		y = 0;
313	}
314	else if (y >= texture->height) {
315		y = texture->height - 1;
316	}
317	sampled = texture->pixels[y * texture->width + x];
318	colour = mesh->vtc[mesh->idc[(u32)pixel->triangleIndex * 3]].col;
319	r = (((sampled >> 11) & 31) * (colour & 0xff) / 255) << 11;
320	g = (((sampled >> 5) & 63) * ((colour >> 8) & 0xff) / 255) << 5;
321	b = (sampled & 31) * ((colour >> 16) & 0xff) / 255;
322	return (sampled & 0xff000000) | r | g | b;
323}
324
325/* convert RGBA8 colour to RGB565
326   returns RGB565 colour */
327static u16 rgba_to_rgb565(u32 rgba)
328{
329	u32 r = (rgba >> 0) & 0xff;
330	u32 g = (rgba >> 8) & 0xff;
331	u32 b = (rgba >> 16) & 0xff;
332
333	return (u16)(((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3));
334}
335
336#ifndef MG_PLATFORM_SDL
337/* convert RGB565 colour to ARGB8
338   returns ARGB8 colour */
339static u32 rgb565_to_argb(u16 rgb)
340{
341	u32 r = (rgb >> 11) & 31;
342	u32 g = (rgb >> 5) & 63;
343	u32 b = rgb & 31;
344
345	r = (r << 3) | (r >> 2);
346	g = (g << 2) | (g >> 4);
347	b = (b << 3) | (b >> 2);
348
349	return 0xff000000 | (r << 16) | (g << 8) | b;
350}
351#endif
352
353/* pack texture pixel for renderer
354   returns packed texture pixel */
355static u32 pack_texture_pixel(u32 rgba)
356{
357	return ((rgba >> 24) << 24) | rgba_to_rgb565(rgba);
358}
359
360void renderer_create(MGContext* ctx)
361{
362	ren.ctx = ctx;
363	ren.pass = 0;
364	ren.active_mesh = NULL;
365	ren.active_texture = NULL;
366	ren.active_triangle = (u32)-1;
367}
368
369void renderer_begin(void)
370{
371	i32 width;
372	i32 height;
373	i32 scaled_width;
374	i32 scaled_height;
375	u64 i;
376	u64 pixel_count;
377	u16 clear_colour = rgba_to_rgb565(0xffb3cce6);
378
379	ren.pass = 0;
380	platform_get_drawable_size(ren.ctx, &width, &height);
381	if (width <= 0 || height <= 0)
382		return;
383
384	scaled_width = width;
385	scaled_height = height;
386	if (scaled_width > SOFTWARE_MAX_WIDTH) {
387		scaled_height = (i32)(((i64)scaled_height * SOFTWARE_MAX_WIDTH) / scaled_width);
388		scaled_width = SOFTWARE_MAX_WIDTH;
389	}
390	if (scaled_height > SOFTWARE_MAX_HEIGHT) {
391		scaled_width = (i32)(((i64)scaled_width * SOFTWARE_MAX_HEIGHT) / scaled_height);
392		scaled_height = SOFTWARE_MAX_HEIGHT;
393	}
394	create_screen_texture(scaled_width, scaled_height);
395
396	pixel_count = (u64)ren.width * (u64)ren.height;
397	for (i = 0; i < pixel_count; i++)
398		ren.framebuffer[i] = clear_colour;
399
400	S3L_newFrame();
401	ren.pass = 1;
402}
403
404void renderer_end(void)
405{
406#ifdef MG_PLATFORM_MAUS
407	i32  x;
408	i32  y;
409	u32* dst;
410#endif
411
412	if (!ren.pass)
413		return;
414
415#ifdef MG_PLATFORM_SDL
416	SDL_UpdateTexture(ren.screen_texture, NULL, ren.framebuffer, ren.width * (i32)sizeof(*ren.framebuffer));
417	SDL_RenderClear(ren.ctx->vbe.renderer);
418	SDL_RenderCopy(ren.ctx->vbe.renderer, ren.screen_texture, NULL, NULL);
419#else
420	dst = ren.ctx->win->bfb;
421	for (y = 0; (u32)y < ren.ctx->win->height; y++) {
422		i32 sy = y * ren.height / ren.ctx->win->height;
423		for (x = 0; (u32)x < ren.ctx->win->width; x++) {
424			i32 sx = x * ren.width / ren.ctx->win->width;
425			dst[(u32)y * ren.ctx->win->stride + (u32)x] =
426			    rgb565_to_argb(ren.framebuffer[sy * ren.width + sx]);
427		}
428	}
429#endif
430	ren.pass = 0;
431}
432
433void renderer_destroy(void)
434{
435	u32 id;
436
437	/* the values could be different in future so
438	   probably should keep them separate */
439	for (id = 1; id < MAX_MESHES; id++)
440		renderer_mesh_destroy(id);
441	for (id = 1; id < MAX_TEXTURES; id++)
442		renderer_texture_destroy(id);
443
444	destroy_screen_texture();
445	free(ren.clip_vertices);
446	free(ren.screen_vertices);
447
448	ren.ctx = NULL;
449	ren.clip_vertices = NULL;
450	ren.screen_vertices = NULL;
451	ren.scratch_capacity = 0;
452	ren.active_mesh = NULL;
453	ren.active_texture = NULL;
454	ren.active_triangle = (u32)-1;
455	ren.pass = 0;
456}
457
458MGMesh renderer_mesh_create(const MGMeshDesc* desc)
459{
460	RendererMesh* mesh;
461	u32           id;
462
463	if (!desc || !desc->vtc || !desc->idc || desc->vtcc == 0 || desc->idcc == 0)
464		return MGMESH_INVALID;
465
466	if (desc->idcc % 3 != 0)
467		return MGMESH_INVALID;
468
469	/* find free mesh slot */
470	for (id = 1; id < MAX_MESHES; id++) {
471		if (!ren.meshes[id].used)
472			break;
473	}
474
475	if (id == MAX_MESHES) {
476		mg_log(LOG_ERR, "Maximum mesh limit reached");
477		return MGMESH_INVALID;
478	}
479
480	/* copy vertex and index data */
481	mesh = &ren.meshes[id];
482	mesh->vtc = malloc(desc->vtcc * sizeof(*mesh->vtc));
483	mesh->idc = malloc(desc->idcc * sizeof(*mesh->idc));
484	if (!mesh->vtc || !mesh->idc) {
485		free(mesh->vtc);
486		free(mesh->idc);
487		*mesh = (RendererMesh){ 0 };
488		mg_log(LOG_ERR, "Failed to allocate mesh data");
489		return MGMESH_INVALID;
490	}
491
492	memcpy(mesh->vtc, desc->vtc, desc->vtcc * sizeof(*mesh->vtc));
493	memcpy(mesh->idc, desc->idc, desc->idcc * sizeof(*mesh->idc));
494	mesh->vc = desc->vtcc;
495	mesh->ic = desc->idcc;
496	mesh->used = 1;
497
498	return id;
499}
500
501void renderer_mesh_draw(MGMesh mesh, MGTexture texture, const MGMat4* mvp)
502{
503	RendererMesh*    rmesh;
504	RendererTexture* rtexture;
505	u32              vertex;
506	u32              tri;
507
508	if (!ren.pass || !mvp)
509		return;
510
511	if (mesh == MGMESH_INVALID || mesh >= MAX_MESHES)
512		return;
513
514	if (texture == MGTEXTURE_INVALID || texture >= MAX_TEXTURES)
515		return;
516
517	rmesh = &ren.meshes[mesh];
518	rtexture = &ren.textures[texture];
519	if (!rmesh->used || !rtexture->used)
520		return;
521
522	if (rmesh->vc > ren.scratch_capacity) {
523		ClipVertex* clip_vertices;
524		S3L_Vec4*   screen_vertices;
525		clip_vertices = (ClipVertex*)realloc(ren.clip_vertices, rmesh->vc * sizeof(*clip_vertices));
526		if (!clip_vertices)
527			return;
528		ren.clip_vertices = clip_vertices;
529		screen_vertices = (S3L_Vec4*)realloc(ren.screen_vertices, rmesh->vc * sizeof(*screen_vertices));
530		if (!screen_vertices)
531			return;
532		ren.screen_vertices = screen_vertices;
533		ren.scratch_capacity = rmesh->vc;
534	}
535	for (vertex = 0; vertex < rmesh->vc; ++vertex) {
536		ren.clip_vertices[vertex] = transform_vertex(mvp, &rmesh->vtc[vertex]);
537		if (ren.clip_vertices[vertex].w > 0.0001f)
538			ren.screen_vertices[vertex] = clip_to_screen(&ren.clip_vertices[vertex]);
539	}
540
541	ren.active_mesh = rmesh;
542	ren.active_texture = rtexture;
543	ren.active_triangle = (u32)-1;
544	ren.texture_sample_count = 0;
545
546	for (tri = 0; tri < rmesh->ic; tri += 3) {
547		u16         ia = rmesh->idc[tri + 0];
548		u16         ib = rmesh->idc[tri + 1];
549		u16         ic = rmesh->idc[tri + 2];
550		ClipVertex* ca = &ren.clip_vertices[ia];
551		ClipVertex* cb = &ren.clip_vertices[ib];
552		ClipVertex* cc = &ren.clip_vertices[ic];
553
554		if (triangle_outside(ca, cb, cc))
555			continue;
556		if (!triangle_front_facing(ca, cb, cc))
557			continue;
558
559		S3L_drawTriangle(ren.screen_vertices[ia], ren.screen_vertices[ib], ren.screen_vertices[ic], 0,
560		                 (S3L_Index)(tri / 3));
561	}
562
563	ren.active_mesh = NULL;
564	ren.active_texture = NULL;
565	ren.active_triangle = (u32)-1;
566}
567
568void renderer_mesh_destroy(MGMesh id)
569{
570	RendererMesh* mesh;
571
572	if (id == MGMESH_INVALID || id >= MAX_MESHES)
573		return;
574
575	mesh = &ren.meshes[id];
576	if (!mesh->used)
577		return;
578
579	free(mesh->idc);
580	free(mesh->vtc);
581	*mesh = (RendererMesh){ 0 };
582}
583
584MGTexture renderer_texture_load(const char* path)
585{
586	stbi_uc*      pixels;
587	int           width;
588	int           height;
589	int           channels;
590	MGTextureDesc desc;
591	MGTexture     texture;
592
593	if (!path)
594		return MGTEXTURE_INVALID;
595	pixels = stbi_load(path, &width, &height, &channels, 4);
596	if (!pixels) {
597		mg_log(LOG_ERR, "Failed to load texture: %s", path);
598		return MGTEXTURE_INVALID;
599	}
600	desc.pixels = pixels;
601	desc.width = width;
602	desc.height = height > width && height % width == 0 ? width : height;
603	texture = renderer_texture_create(&desc);
604	stbi_image_free(pixels);
605	return texture;
606}
607
608MGTexture renderer_texture_create(const MGTextureDesc* desc)
609{
610	RendererTexture* texture;
611	size_t           size;
612	size_t           pixel_count;
613	size_t           i;
614	u32              id;
615
616	if (!desc || !desc->pixels || desc->width <= 0 || desc->height <= 0)
617		return MGTEXTURE_INVALID;
618	for (id = 1; id < MAX_TEXTURES; id++)
619		if (!ren.textures[id].used)
620			break;
621
622	if (id == MAX_TEXTURES) {
623		mg_log(LOG_ERR, "Maximum texture limit reached");
624		return MGTEXTURE_INVALID;
625	}
626
627	texture = &ren.textures[id];
628	pixel_count = (size_t)desc->width * (size_t)desc->height;
629	size = pixel_count * sizeof(*texture->pixels);
630	texture->pixels = (u32*)malloc(size);
631	if (!texture->pixels) {
632		mg_log(LOG_ERR, "Failed to create texture");
633		return MGTEXTURE_INVALID;
634	}
635
636	for (i = 0; i < pixel_count; ++i) {
637		const u8* source = desc->pixels + i * 4;
638		u32 rgba = (u32)source[0] | ((u32)source[1] << 8) | ((u32)source[2] << 16) | ((u32)source[3] << 24);
639		texture->pixels[i] = pack_texture_pixel(rgba);
640	}
641	texture->width = desc->width;
642	texture->height = desc->height;
643	texture->used = 1;
644
645	return id;
646}
647
648void renderer_texture_destroy(MGTexture id)
649{
650	RendererTexture* texture;
651
652	if (id == MGTEXTURE_INVALID || id >= MAX_TEXTURES)
653		return;
654
655	texture = &ren.textures[id];
656	if (!texture->used)
657		return;
658
659	free(texture->pixels);
660	*texture = (RendererTexture){ 0 };
661}
662
663void renderer_debug_wireframe(i8 enabled)
664{
665	/* do nothing; win */
666	(void)enabled;
667}