From 023a6b31e63cdf01718f86844bd7f69dd542fb78 Mon Sep 17 00:00:00 2001 From: Andrei Alexeyev Date: Wed, 21 Aug 2024 23:24:19 +0200 Subject: [PATCH] test/renderer/texture: add a basic textured quad test --- test/renderer/meson.build | 1 + test/renderer/texture.c | 161 ++++++++++++++++++++++++++++++++++++ test/renderer/texture0.webp | Bin 0 -> 14328 bytes 3 files changed, 162 insertions(+) create mode 100644 test/renderer/texture.c create mode 100644 test/renderer/texture0.webp diff --git a/test/renderer/meson.build b/test/renderer/meson.build index 42c8cb99..0c12f527 100644 --- a/test/renderer/meson.build +++ b/test/renderer/meson.build @@ -1,6 +1,7 @@ tests = [ 'triangle', + 'texture', ] foreach test : tests diff --git a/test/renderer/texture.c b/test/renderer/texture.c new file mode 100644 index 00000000..37e1ca08 --- /dev/null +++ b/test/renderer/texture.c @@ -0,0 +1,161 @@ + +#include "taisei.h" + +#include "test_renderer.h" +#include "global.h" + +typedef struct Vertex2D { + vec2 pos; + vec2 uv; + vec4 color; +} Vertex2D; + +static Texture *load_texture(const char *path) { + auto istream = SDL_RWFromFile(path, "rb"); + + if(!istream) { + log_sdl_error(LOG_FATAL, "SDL_IOFromFile"); + } + + Pixmap px = {}; + PixmapFormat fmt = PIXMAP_FORMAT_RGBA8; + PixmapOrigin origin = PIXMAP_ORIGIN_TOPLEFT; + + if(r_features() & RFEAT_TEXTURE_BOTTOMLEFT_ORIGIN) { + origin = PIXMAP_ORIGIN_BOTTOMLEFT; + } + + if(!pixmap_load_stream(istream, PIXMAP_FILEFORMAT_AUTO, &px, fmt)) { + log_fatal("pixmap_load_stream() failed"); + } + + SDL_RWclose(istream); + + pixmap_convert_inplace_realloc(&px, fmt); + pixmap_flip_to_origin_inplace(&px, origin); + + Texture *tex = r_texture_create(&(TextureParams) { + .class = TEXTURE_CLASS_2D, + .type = r_texture_type_from_pixmap_format(fmt), + .width = px.width, + .height = px.height, + .layers = 1, + .filter.min = TEX_FILTER_LINEAR, + .filter.mag = TEX_FILTER_LINEAR, + .wrap.s = TEX_WRAP_REPEAT, + .wrap.t = TEX_WRAP_REPEAT, + }); + + if(!tex) { + log_fatal("r_texture_create() failed"); + } + + r_texture_fill(tex, 0, 0, &px); + mem_free(px.data.untyped); + + return tex; +} + +int main(int argc, char **argv) { + test_init_renderer(); + time_init(); + + FPSCounter fps = {}; + + const char *vert_shader_src = + "#version 420\n" + "layout(location = 0) in vec2 a_position;\n" + "layout(location = 1) in vec2 a_uv;\n" + "layout(location = 2) in vec4 a_color;\n" + "layout(location = 0) out vec2 v_uv;\n" + "layout(location = 1) out vec4 v_color;\n" + "void main(void) {\n" + " gl_Position = vec4(a_position, 0, 1);\n" + " v_uv = a_uv;\n" + " v_color = a_color;\n" + "}\n"; + + const char *frag_shader_src = + "#version 420\n" + "layout(location = 0) in vec2 v_uv;\n" + "layout(location = 1) in vec4 v_color;\n" + "layout(location = 0) out vec4 o_color;\n" + "uniform sampler2D u_sampler;\n" + "void main(void) {\n" + " o_color = texture(u_sampler, v_uv) * v_color;\n" + "}\n"; + + ShaderObject *vert_obj = test_renderer_load_glsl(SHADER_STAGE_VERTEX, vert_shader_src); + ShaderObject *frag_obj = test_renderer_load_glsl(SHADER_STAGE_FRAGMENT, frag_shader_src); + ShaderProgram *prog = r_shader_program_link(2, (ShaderObject*[]) { vert_obj, frag_obj }); + r_shader_object_destroy(vert_obj); + r_shader_object_destroy(frag_obj); + + VertexAttribSpec va_spec[] = { + { 2, VA_FLOAT, VA_CONVERT_FLOAT }, + { 2, VA_FLOAT, VA_CONVERT_FLOAT }, + { 4, VA_FLOAT, VA_CONVERT_FLOAT }, + }; + + VertexAttribFormat va_format[ARRAY_SIZE(va_spec)]; + r_vertex_attrib_format_interleaved(ARRAY_SIZE(va_spec), va_spec, va_format, 0); + + Vertex2D vertices[] = { + // { { 1, -1, }, { 1, 1 }, { 1, 0, 0, 1 }, }, + // { { 1, 1, }, { 1, 0 }, { 0, 1, 0, 1 }, }, + // { { -1, -1, }, { 0, 1 }, { 0, 0, 1, 1 }, }, + // { { -1, 1, }, { 0, 0 }, { 1, 1, 0, 1 }, }, + + { { 1, -1, }, { 1, 0 }, { 1, 0, 0, 1 }, }, + { { 1, 1, }, { 1, 1 }, { 0, 1, 0, 1 }, }, + { { -1, -1, }, { 0, 0 }, { 0, 0, 1, 1 }, }, + { { -1, 1, }, { 0, 1 }, { 1, 1, 0, 1 }, }, + }; + + VertexBuffer *vert_buf = r_vertex_buffer_create(sizeof(vertices), vertices); + r_vertex_buffer_set_debug_label(vert_buf, "Triangle vertex buffer"); + + VertexArray *vert_array = r_vertex_array_create(); + r_vertex_array_set_debug_label(vert_array, "Triangle vertex array"); + r_vertex_array_layout(vert_array, ARRAY_SIZE(va_format), va_format); + r_vertex_array_attach_vertex_buffer(vert_array, vert_buf, 0); + + Model triangle = { + .primitive = PRIM_TRIANGLE_STRIP, + .vertex_array = vert_array, + .num_vertices = ARRAY_SIZE(vertices), + .offset = 0, + }; + + Texture *tex = load_texture("texture0.webp"); + + r_shader_ptr(prog); + auto u_sampler = r_shader_uniform(prog, "u_sampler"); + + hrtime_t last_print_time = 0; + + while(!taisei_quit_requested()) { + r_clear(BUFFER_COLOR, RGB(0, 0, 0), 1); + r_uniform_sampler(u_sampler, tex); + r_draw_model_ptr(&triangle, 1, 0); + events_poll(NULL, 0); + video_swap_buffers(); + + fpscounter_update(&fps); + + hrtime_t t = time_get(); + if(t - last_print_time > HRTIME_RESOLUTION) { + last_print_time = t; + log_debug("%.02f FPS", fps.fps); + } + } + + r_vertex_buffer_destroy(vert_buf); + r_vertex_array_destroy(vert_array); + + r_shader_program_destroy(prog); + + time_shutdown(); + test_shutdown_renderer(); + return 0; +} diff --git a/test/renderer/texture0.webp b/test/renderer/texture0.webp new file mode 100644 index 0000000000000000000000000000000000000000..12190aae5eb8db368507693461fa091132d047f7 GIT binary patch literal 14328 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