From 139394c6de5f1d8f8d2d2fc95abc1a563ee8178b Mon Sep 17 00:00:00 2001 From: veclavtalica Date: Tue, 15 Oct 2024 15:29:45 +0300 Subject: [PATCH] partial implementation of double buffered render --- src/rendering/twn_draw.c | 10 +- src/rendering/twn_draw_c.h | 8 +- src/rendering/twn_gl_15_rendering.c | 322 +++++++++++++++++++++++---- src/rendering/twn_gl_any_rendering.c | 28 +-- src/rendering/twn_rects.c | 4 +- src/rendering/twn_sprites.c | 7 +- src/rendering/twn_text.c | 4 +- src/rendering/twn_triangles.c | 4 +- src/twn_engine_context_c.h | 1 + src/twn_loop.c | 2 + 10 files changed, 294 insertions(+), 96 deletions(-) diff --git a/src/rendering/twn_draw.c b/src/rendering/twn_draw.c index bf0ff69..38e98b5 100644 --- a/src/rendering/twn_draw.c +++ b/src/rendering/twn_draw.c @@ -374,11 +374,11 @@ void render(void) { } } - render_space(); - render_skybox(); /* after space, as to use depth buffer for early rejection */ - render_2d(); - swap_buffers(); - clear_draw_buffer(); + start_render_frame(); { + render_space(); + render_skybox(); /* after space, as to use depth buffer for early z rejection */ + render_2d(); + } end_render_frame(); } diff --git a/src/rendering/twn_draw_c.h b/src/rendering/twn_draw_c.h index f4adb0c..be2be9e 100644 --- a/src/rendering/twn_draw_c.h +++ b/src/rendering/twn_draw_c.h @@ -165,6 +165,8 @@ void text_cache_reset_arena(TextCache *cache); VertexBuffer create_vertex_buffer(void); +VertexBuffer get_scratch_vertex_array(void); + void delete_vertex_buffer(VertexBuffer buffer); void specify_vertex_buffer(VertexBuffer buffer, void const *data, size_t bytes); @@ -187,7 +189,7 @@ void swap_buffers(void); void set_depth_range(double low, double high); -void bind_quad_element_buffer(void); +VertexBuffer get_quad_element_buffer(void); void render_circle(const CirclePrimitive *circle); @@ -238,4 +240,8 @@ void pop_fog(void); void finally_pop_fog(void); +void start_render_frame(void); + +void end_render_frame(void); + #endif diff --git a/src/rendering/twn_gl_15_rendering.c b/src/rendering/twn_gl_15_rendering.c index ccc8626..82e0c9f 100644 --- a/src/rendering/twn_gl_15_rendering.c +++ b/src/rendering/twn_gl_15_rendering.c @@ -95,6 +95,58 @@ typedef struct ElementIndexedQuadWithoutColorWithoutTexture { } ElementIndexedQuadWithoutColorWithoutTexture; +typedef struct { + size_t offset; + GLenum type; + GLsizei stride; + GLuint buffer; + uint8_t arity; /* leave at 0 to signal pointer as unused */ +} AttributeArrayPointer; + + +/* allows us to have generic way to issue draws as well as */ +/* deferring new draw calls while previous frame is still being drawn */ +typedef struct { + AttributeArrayPointer vertices; + AttributeArrayPointer texcoords; + + bool constant_colored; + union { + AttributeArrayPointer colors; + Color color; + }; + + bool textured, texture_repeat; + TextureKey texture; + TextureMode texture_mode; + + GLuint element_buffer; + GLsizei element_count; + GLsizei range_start, range_end; +} DeferredCommandDrawElements; + + +typedef struct { + Color color; + bool clear_color; + bool clear_depth; + bool clear_stencil; +} DeferredCommandClear; + + +typedef struct { + enum DeferredCommandType { + DEFERRED_COMMAND_TYPE_DRAW_ELEMENTS, + DEFERRED_COMMAND_TYPE_CLEAR, + } type; + + union { + DeferredCommandDrawElements draw_elements; + DeferredCommandClear clear; + }; +} DeferredCommand; + + typedef enum { PIPELINE_NO, PIPELINE_SPACE, @@ -108,6 +160,186 @@ static Pipeline pipeline_last_used = PIPELINE_NO; #define CIRCLE_VERTICES_MAX 2048 +/* potentially double buffered array of vertex array handles */ +/* we assume they will be refilled fully each frame */ +static size_t scratch_va_front_used, scratch_va_back_used; +static GLuint *front_scratch_vertex_arrays, *back_scratch_vertex_arrays; +static GLuint **current_scratch_vertex_array = &front_scratch_vertex_arrays; +static void restart_scratch_vertex_arrays(void) { + scratch_va_front_used = 0; + scratch_va_back_used = 0; + + if (ctx.render_double_buffered) + current_scratch_vertex_array = current_scratch_vertex_array == &front_scratch_vertex_arrays ? + &back_scratch_vertex_arrays : &front_scratch_vertex_arrays; +} + + +GLuint get_scratch_vertex_array(void) { + size_t *used = current_scratch_vertex_array == &front_scratch_vertex_arrays ? + &scratch_va_front_used : &scratch_va_back_used; + + if (arrlenu(*current_scratch_vertex_array) <= *used) { + GLuint handle; + glGenBuffers(1, &handle); + arrpush(*current_scratch_vertex_array, handle); + } + + (*used)++; + return (*current_scratch_vertex_array)[arrlenu(*current_scratch_vertex_array) - 1]; +} + + +static DeferredCommand *deferred_commands; +static void issue_deferred_draw_commands(void) { + use_2d_pipeline(); + + for (size_t i = 0; i < arrlenu(deferred_commands); ++i) { + switch (deferred_commands[i].type) { + case DEFERRED_COMMAND_TYPE_CLEAR: { + glClearColor((1.0f / 255) * deferred_commands[i].clear.color.r, + (1.0f / 255) * deferred_commands[i].clear.color.g, + (1.0f / 255) * deferred_commands[i].clear.color.b, + (1.0f / 255) * deferred_commands[i].clear.color.a); + + /* needed as we might mess with it */ + glDepthRange(0.0, 1.0); + glDepthMask(GL_TRUE); + + glClear((deferred_commands[i].clear.clear_color ? GL_COLOR_BUFFER_BIT : 0) | + (deferred_commands[i].clear.clear_depth ? GL_DEPTH_BUFFER_BIT : 0) | + (deferred_commands[i].clear.clear_stencil ? GL_STENCIL_BUFFER_BIT : 0) ); + break; + } + + case DEFERRED_COMMAND_TYPE_DRAW_ELEMENTS: { + DeferredCommandDrawElements const command = deferred_commands[i].draw_elements; + + /* TODO: don't assume a single vertex array ? */ + SDL_assert(command.vertices.arity != 0); + SDL_assert(command.vertices.buffer); + SDL_assert(command.element_count != 0); + SDL_assert(command.element_buffer); + + glBindBuffer(GL_ARRAY_BUFFER, command.vertices.buffer); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, command.element_buffer); + + glEnableClientState(GL_VERTEX_ARRAY); + glVertexPointer(command.vertices.arity, + command.vertices.type, + command.vertices.stride, + (void *)command.vertices.offset); + + use_texture_mode(command.texture_mode); + + if (command.texcoords.arity != 0) { + SDL_assert(command.texcoords.buffer == command.vertices.buffer); + + glEnableClientState(GL_TEXTURE_COORD_ARRAY); + glClientActiveTexture(GL_TEXTURE0); + glTexCoordPointer(command.texcoords.arity, + command.texcoords.type, + command.texcoords.stride, + (void *)command.texcoords.offset); + } + + if (command.colors.arity != 0) { + SDL_assert(command.colors.buffer == command.vertices.buffer); + + glEnableClientState(GL_COLOR_ARRAY); + glColorPointer(command.colors.arity, + command.colors.type, + command.colors.stride, + (void *)command.colors.offset); + } else if (command.constant_colored) + glColor4ub(command.color.r, + command.color.g, + command.color.b, + command.color.a); + + if (command.textured) { + if (command.texture_repeat) + textures_bind_repeating(&ctx.texture_cache, command.texture); + else + textures_bind(&ctx.texture_cache, command.texture); + } + + if (command.range_start == command.range_end) + glDrawElements(GL_TRIANGLES, command.element_count, GL_UNSIGNED_SHORT, NULL); + else + glDrawRangeElements(GL_TRIANGLES, + command.range_start, + command.range_end, + command.element_count, + GL_UNSIGNED_SHORT, + NULL); + + /* state clearing */ + + if (command.textured) + glBindTexture(GL_TEXTURE_2D, 0); + + if (command.colors.arity != 0) + glDisableClientState(GL_COLOR_ARRAY); + + if (command.texcoords.arity != 0) + glDisableClientState(GL_TEXTURE_COORD_ARRAY); + + glDisableClientState(GL_VERTEX_ARRAY); + + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + + break; + } + + default: + SDL_assert(false); + } + } +} + + +void clear_draw_buffer(void) { + /* TODO: we can optimize a rectangle drawn over whole window to a clear color call*/ + + DeferredCommand command = { + .type = DEFERRED_COMMAND_TYPE_CLEAR, + .clear = (DeferredCommandClear) { + .clear_color = true, + .clear_depth = true, + .clear_stencil = true, + .color = (Color) { 230, 230, 230, 1 } + } + }; + + arrpush(deferred_commands, command); +} + + +void start_render_frame(void) { + restart_scratch_vertex_arrays(); + clear_draw_buffer(); +} + + +void end_render_frame(void) { + if (!ctx.render_double_buffered || ctx.game.frame_number == 0) { + issue_deferred_draw_commands(); + SDL_GL_SwapWindow(ctx.window); + arrsetlen(deferred_commands, 0); + } else { + /* instead of waiting for frame to finish for the swap, we continue */ + /* while issuing new state for the next call, but deferring any fragment emitting calls for later */ + /* actual swap will happen when next frame is fully finished, introducing a delay */ + SDL_GL_SwapWindow(ctx.window); + issue_deferred_draw_commands(); + glFlush(); + arrsetlen(deferred_commands, 0); + } +} + + void use_space_pipeline(void) { if (pipeline_last_used == PIPELINE_SPACE) return; @@ -301,7 +533,7 @@ void finally_render_quads(const Primitive2D primitives[], const struct QuadBatch batch, const VertexBuffer buffer) { - (void)buffer; + DeferredCommandDrawElements command = {0}; GLsizei off = 0, voff = 0, uvoff = 0, coff = 0; @@ -323,63 +555,55 @@ void finally_render_quads(const Primitive2D primitives[], voff = offsetof(ElementIndexedQuadWithoutColorWithoutTexture, v0); } - /* vertex specification */ - glEnableClientState(GL_VERTEX_ARRAY); - glVertexPointer(2, - GL_FLOAT, - off, - (void *)(size_t)voff); + command.vertices = (AttributeArrayPointer) { + .arity = 2, + .type = GL_FLOAT, + .stride = off, + .offset = voff, + .buffer = buffer + }; - if (batch.textured) { - glEnableClientState(GL_TEXTURE_COORD_ARRAY); - glClientActiveTexture(GL_TEXTURE0); - glTexCoordPointer(2, - GL_FLOAT, - off, - (void *)(size_t)uvoff); - } + if (batch.textured) + command.texcoords = (AttributeArrayPointer) { + .arity = 2, + .type = GL_FLOAT, + .stride = off, + .offset = uvoff, + .buffer = buffer + }; if (!batch.constant_colored) { - glEnableClientState(GL_COLOR_ARRAY); - glColorPointer(4, - GL_UNSIGNED_BYTE, - off, - (void *)(size_t)coff); - } else - glColor4ub(primitives[0].sprite.color.r, - primitives[0].sprite.color.g, - primitives[0].sprite.color.b, - primitives[0].sprite.color.a); + command.colors = (AttributeArrayPointer) { + .arity = 4, + .type = GL_UNSIGNED_BYTE, + .stride = off, + .offset = coff, + .buffer = buffer + }; + } else { + command.constant_colored = true; + command.color = primitives[0].sprite.color; + } if (batch.textured) { - if (!batch.repeat) - textures_bind(&ctx.texture_cache, primitives->sprite.texture_key); - else - textures_bind_repeating(&ctx.texture_cache, primitives->sprite.texture_key); + /* TODO: bad, don't */ + command.textured = true; + command.texture = primitives->sprite.texture_key; + command.texture_repeat = batch.repeat; } - bind_quad_element_buffer(); + command.element_buffer = get_quad_element_buffer(); + command.element_count = 6 * (GLsizei)batch.size; + command.range_end = 6 * (GLsizei)batch.size; - glDrawElements(GL_TRIANGLES, 6 * (GLsizei)batch.size, GL_UNSIGNED_SHORT, NULL); + command.texture_mode = batch.mode; - /* clear the state */ - { - glBufferData(GL_ARRAY_BUFFER, 0, NULL, GL_STREAM_DRAW); - glBindBuffer(GL_ARRAY_BUFFER, 0); + DeferredCommand final_command = { + .type = DEFERRED_COMMAND_TYPE_DRAW_ELEMENTS, + .draw_elements = command + }; - if (batch.textured) - glDisableClientState(GL_TEXTURE_COORD_ARRAY); - - glDisableClientState(GL_VERTEX_ARRAY); - - if (!batch.constant_colored) - glDisableClientState(GL_COLOR_ARRAY); - - if (batch.textured) - glBindTexture(GL_TEXTURE_2D, 0); - - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); - } + arrpush(deferred_commands, final_command); } @@ -553,7 +777,7 @@ void finally_draw_text(FontData const *font_data, offsetof(ElementIndexedQuadWithoutColor, v1), (void *)(size_t)offsetof(ElementIndexedQuadWithoutColor, uv0)); - bind_quad_element_buffer(); + get_quad_element_buffer(); use_texture_mode(TEXTURE_MODE_GHOSTLY); diff --git a/src/rendering/twn_gl_any_rendering.c b/src/rendering/twn_gl_any_rendering.c index c3d0b30..5b49cb9 100644 --- a/src/rendering/twn_gl_any_rendering.c +++ b/src/rendering/twn_gl_any_rendering.c @@ -32,7 +32,7 @@ void specify_vertex_buffer(VertexBuffer buffer, void const *data, size_t bytes) } -void bind_quad_element_buffer(void) { +VertexBuffer get_quad_element_buffer(void) { static GLuint buffer = 0; /* it's only generated once at runtime */ @@ -62,32 +62,8 @@ void bind_quad_element_buffer(void) { } else glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffer); -} - -void clear_draw_buffer(void) { - /* TODO: we can optimize a rectangle drawn over whole window to a clear color call*/ - glClearColor((1.0f / 255) * 230, - (1.0f / 255) * 230, - (1.0f / 255) * 230, 1); - - glDepthRange(0.0, 1.0); - glDepthMask(GL_TRUE); - - /* TODO: don't clear color when skybox is applied? */ - /* for that window should match framebuffer */ - /* also it is driver dependent, from what i can gather */ - /* INFO: also, based on below, driver might prefer it staying this way */ - /* https://gamedev.stackexchange.com/questions/90344/render-with-const-depth-value */ - /* we could optionally load ARB_invalidate_subdata extension if it's available instead */ - glClear(GL_COLOR_BUFFER_BIT | - GL_DEPTH_BUFFER_BIT | - GL_STENCIL_BUFFER_BIT); -} - - -void swap_buffers(void) { - SDL_GL_SwapWindow(ctx.window); + return buffer; } diff --git a/src/rendering/twn_rects.c b/src/rendering/twn_rects.c index feacee8..3d88f11 100644 --- a/src/rendering/twn_rects.c +++ b/src/rendering/twn_rects.c @@ -72,9 +72,7 @@ void render_rect_batch(const Primitive2D primitives[], SDL_assert(primitives[0].type == PRIMITIVE_2D_RECT); /* single vertex array is used for every batch with NULL glBufferData() trick at the end */ - static VertexBuffer vertex_array = 0; - if (vertex_array == 0) - vertex_array = create_vertex_buffer(); + VertexBuffer const vertex_array = get_scratch_vertex_array(); use_texture_mode(batch.mode); diff --git a/src/rendering/twn_sprites.c b/src/rendering/twn_sprites.c index 20cc7fd..32e4704 100644 --- a/src/rendering/twn_sprites.c +++ b/src/rendering/twn_sprites.c @@ -125,12 +125,7 @@ void render_sprite_batch(const Primitive2D primitives[], SDL_assert(primitives && batch.size != 0); SDL_assert(primitives[0].type == PRIMITIVE_2D_SPRITE); - /* single vertex array is used for every batch with NULL glBufferData() trick at the end */ - static VertexBuffer vertex_array = 0; - if (vertex_array == 0) - vertex_array = create_vertex_buffer(); - - use_texture_mode(batch.mode); + VertexBuffer const vertex_array = get_scratch_vertex_array(); const Rect dims = textures_get_dims(&ctx.texture_cache, primitives->sprite.texture_key); diff --git a/src/rendering/twn_text.c b/src/rendering/twn_text.c index 82cb153..d72acc7 100644 --- a/src/rendering/twn_text.c +++ b/src/rendering/twn_text.c @@ -171,9 +171,7 @@ static void text_destroy_font_data(FontData *font_data) { static void text_draw_with(FontData* font_data, char* text, Vec2 position, Color color) { - VertexBuffer vertex_array = 0; - if (vertex_array == 0) - vertex_array = create_vertex_buffer(); + VertexBuffer const vertex_array = get_scratch_vertex_array(); const size_t len = SDL_strlen(text); diff --git a/src/rendering/twn_triangles.c b/src/rendering/twn_triangles.c index 9d445b2..70afdfe 100644 --- a/src/rendering/twn_triangles.c +++ b/src/rendering/twn_triangles.c @@ -45,9 +45,7 @@ void draw_triangle(const char *path, void draw_uncolored_space_traingle_batch(struct MeshBatch *batch, TextureKey texture_key) { - static VertexBuffer vertex_array = 0; - if (vertex_array == 0) - vertex_array = create_vertex_buffer(); + VertexBuffer const vertex_array = get_scratch_vertex_array(); const size_t primitives_len = arrlenu(batch->primitives); diff --git a/src/twn_engine_context_c.h b/src/twn_engine_context_c.h index 9c8104d..e11ee97 100644 --- a/src/twn_engine_context_c.h +++ b/src/twn_engine_context_c.h @@ -77,6 +77,7 @@ typedef struct EngineContext { bool window_size_has_changed; bool resync_flag; bool was_successful; + bool render_double_buffered; } EngineContext; /* TODO: does it need to be marked with TWN_API? */ diff --git a/src/twn_loop.c b/src/twn_loop.c index d1ad66c..c2c4fbb 100644 --- a/src/twn_loop.c +++ b/src/twn_loop.c @@ -658,6 +658,8 @@ static bool initialize(void) { } */ + ctx.render_double_buffered = true; + return true; fail: