M include/forge/gui_draw.h => include/forge/gui_draw.h +10 -0
@@ 32,4 32,14 @@ fg_draw_gui_circle(
fg_vec2 const center,
fg_real radius);
+/*
+ * Add line primitive to primitive buffer.
+ */
+void
+fg_draw_gui_line(
+ struct fg_gui_context* context,
+ struct fg_gui_style const* style,
+ fg_vec2 const begin,
+ fg_vec2 const end);
+
#endif // FORGE_GUI_DRAW_H
M include/forge/gui_type.h => include/forge/gui_type.h +42 -0
@@ 145,6 145,35 @@ struct fg_gui_rect {
};
/*
+ * Packed line/curve representation.
+ * Struct members are packed to increase GPU throughput.
+ */
+struct fg_gui_line {
+ // Adhere to std430 shader storage buffer alignment rules
+ alignas(32)
+ /*
+ * 4 floats.
+ * First two values are X, Y in window coordinates, see
+ * [[fg_gui_rect.rect]]. Last two values are length in X, Y respectively.
+ */
+ float line[4];
+ // unorm4x8: R, G, B, A.
+ uint32_t color;
+ /*
+ * 32-bit signed integer.
+ * Draw order for the given line. This is a global value across all drawn
+ * elements that is mapped to a corresponding depth value.
+ */
+ int32_t order;
+ /*
+ * 32-bit unsigned integer.
+ * 24 bits for the clip index, and the remaining 8 bits are currently
+ * reserved to specify curve types.
+ */
+ uint32_t flags;
+};
+
+/*
* Fixed-sized buffer for GUI rects with pre-allocated storage.
*
* Buffer memory is allocated once at the start of the program lifetime and
@@ 166,6 195,17 @@ struct fg_gui_rect_buffer {
};
/*
+ * Fixed-size buffer for GUI lines/curves with pre-allocated storage.
+ * See [[fg_gui_rect_buffer]] for more details.
+ */
+struct fg_gui_line_buffer {
+ struct fg_gui_line* prims;
+ int capacity;
+ int size_front;
+ int size_back;
+};
+
+/*
* Monolithic struct storing GUI state.
*
* Most GUI functions receive the context as the first parameter. The context
@@ 224,6 264,8 @@ struct fg_gui_context {
// GUI rects.
struct fg_gui_rect_buffer rects;
+ // GUI lines.
+ struct fg_gui_line_buffer lines;
};
#endif // FORGE_GUI_TYPE_H
A shader/gui_line.frag.glsl => shader/gui_line.frag.glsl +21 -0
@@ 0,0 1,21 @@
+// This file is part of Forge, the foundation library for Forge tools.
+//
+// Copyright (C) 2021 Ryan Chan <rycwo@posteo.net>
+// SPDX-License-Identifier: GPL-3.0-only
+//
+// This Source Code Form is subject to the terms of the GNU General Public
+// License v3.0 only. You should have received a copy of the license along with
+// this program. If not, see <https://www.gnu.org/licenses/>.
+
+#version 460 core
+
+layout(location = 0) in vertex_attr {
+ flat vec4 color;
+} frag_in;
+
+layout(location = 0) out vec4 color;
+
+void
+main(void) {
+ color = frag_in.color;
+}
A shader/gui_line.vert.glsl => shader/gui_line.vert.glsl +58 -0
@@ 0,0 1,58 @@
+// This file is part of Forge, the foundation library for Forge tools.
+//
+// Copyright (C) 2021 Ryan Chan <rycwo@posteo.net>
+// SPDX-License-Identifier: GPL-3.0-only
+//
+// This Source Code Form is subject to the terms of the GNU General Public
+// License v3.0 only. You should have received a copy of the license along with
+// this program. If not, see <https://www.gnu.org/licenses/>.
+
+#version 460 core
+
+// See struct gui_line in forge/gui_type.h
+struct primitive {
+ vec4 line; // TODO: Curves
+ uint color;
+ int order;
+ uint flags;
+};
+
+layout(std140, binding = 0) uniform context {
+ mat4 projection;
+ vec2 viewport;
+};
+
+layout(std430, binding = 0) restrict readonly buffer clip_buffer {
+ vec4 clips[];
+};
+
+layout(std430, binding = 1) restrict readonly buffer prim_buffer {
+ primitive prims[];
+};
+
+layout(location = 0) out vertex_attr {
+ flat vec4 color;
+} vert_out;
+
+const int clip_mask = 0xFFFFFF;
+
+void
+main(void) {
+ primitive prim = prims[gl_VertexID / 2];
+
+ vec2 position = prim.line.xy;
+ if (!bool(gl_VertexID & 1)) {
+ position += prim.line.zw;
+ }
+
+ // Apply clip rect
+ uint clip_index = prim.flags & clip_mask;
+ vec4 clip = clips[clip_index];
+ position = clamp(position, clip.xy, clip.xy + clip.zw);
+
+ float depth = (1.0 - (float(prim.order) / float(0xFFFFFF)) * 2.0) - 1.0;
+
+ gl_Position = projection * vec4(position, depth, 1.0);
+
+ vert_out.color = unpackUnorm4x8(prim.color);
+}
M src/gui_context.c => src/gui_context.c +13 -0
@@ 51,6 51,12 @@ fg_init_gui(
context->rects.size_front = 0;
context->rects.size_back = 0;
+ context->lines.prims = (struct fg_gui_line*)alloc->alloc(
+ sizeof(struct fg_gui_line) * desc.line_buffer_size);
+ context->lines.capacity = desc.line_buffer_size;
+ context->lines.size_front = 0;
+ context->lines.size_back = 0;
+
// Graphics backend. For now it is hard-coded to use OpenGL 4.6.
fg_init_gui_opengl_46(context);
}
@@ 59,6 65,7 @@ void
fg_terminate_gui(struct fg_gui_context* context) {
context->alloc.free(context->clips.buf);
context->alloc.free(context->rects.prims);
+ context->alloc.free(context->lines.prims);
memset(context, 0, sizeof(struct fg_gui_context));
fg_term_gui_opengl_46();
@@ 81,6 88,8 @@ fg_begin_gui_frame(struct fg_gui_context* context) {
context->clips.size = 1;
context->rects.size_front = 0;
context->rects.size_back = 0;
+ context->lines.size_front = 0;
+ context->lines.size_back = 0;
}
void
@@ 111,6 120,10 @@ fg_commit_gui_frame(struct fg_gui_context* context) {
context->rects.prims[context->rects.size_front + i]
= context->rects.prims[context->rects.capacity - i - 1];
+ for (int i = 0; i < context->lines.size_back; ++i)
+ context->lines.prims[context->lines.size_front + i]
+ = context->lines.prims[context->lines.capacity - i - 1];
+
fg_commit_gui_opengl_46(context);
}
M src/gui_draw.c => src/gui_draw.c +85 -0
@@ 17,6 17,7 @@
#include "forge/gui_util.h"
#include "forge/pack_float.h"
+#include "forge/vector.h"
#define CLIP_MASK (0xFFFFFF)
@@ 33,6 34,16 @@ next_prim_id(struct fg_gui_context* context) {
return (context->base_count)++;
}
+static void
+rect_vert(fg_vec2 out, int vertex, fg_vec2 const v1, fg_vec2 const v2) {
+ // 0: v1.x, v1.y
+ // 1: v2.x, v1.y
+ // 2: v2.x, v2.y
+ // 3: v1.x, v2.y
+ out[0] = (vertex % 3 == 0) ? v1[0] : v2[0];
+ out[1] = (vertex < 2) ? v1[1] : v2[1];
+}
+
static bool
make_gui_rect(
struct fg_gui_context* context,
@@ 76,6 87,48 @@ push_rect(struct fg_gui_context* context, struct fg_gui_rect prim) {
buffer->prims[(buffer->size_front)++] = prim;
}
+static bool
+make_gui_line(
+ struct fg_gui_context* context,
+ struct fg_gui_style const* style,
+ fg_vec2 const begin,
+ fg_vec2 const end,
+ struct fg_gui_line* prim) {
+ memset(prim, 0, sizeof(struct fg_gui_line));
+ // Filter out transparent lines
+ if (fabs(style->color[3]) <= 1e-6) // FIXME
+ return false;
+
+ // Lines are defined similarly to rects so we can treat it as a rect for
+ // transformation purposes.
+ fg_vec2 dims;
+ fg_vec2_sub(dims, end, begin);
+ fg_vec4 line;
+ fg_gui_transform_rect(
+ context, line, (fg_vec4){begin[0], begin[1], dims[0], dims[1]});
+ for (int i = 0; i < 4; ++i)
+ prim->line[i] = (float)line[i];
+
+ float color[4];
+ for (int i = 0; i < 4; ++i)
+ color[i] = (float)style->color[i];
+ prim->color = fg_pack_unorm_4x8(color);
+ prim->order = next_prim_id(context);
+ prim->flags = ((uint32_t)context->clip) & CLIP_MASK;
+ return true;
+}
+
+static inline void
+push_line(struct fg_gui_context* context, struct fg_gui_line prim) {
+ struct fg_gui_line_buffer* buffer = &context->lines;
+ if (context->layer == FG_GUI_LAYER_OVERLAY) {
+ int const size = (buffer->size_back)++;
+ buffer->prims[buffer->capacity - size - 1] = prim;
+ return;
+ }
+ buffer->prims[(buffer->size_front)++] = prim;
+}
+
void
fg_draw_gui_rect(
struct fg_gui_context* context,
@@ 88,6 141,22 @@ fg_draw_gui_rect(
if (!make_gui_rect(context, style, rect, &prim))
return;
push_rect(context, prim);
+
+ bool const border = style->flags & FG_GUI_STYLE_BORDER;
+ if (border) {
+ fg_vec2 const v1 = {rect[0], rect[1]};
+ fg_vec2 const v2 = {rect[0] + rect[2], rect[1] + rect[3]};
+ // TODO: Border width
+ struct fg_gui_style style_;
+ fg_vec4_copy(style_.color, style->border_color);
+ for (int i = 0; i < 4; ++i) {
+ fg_vec2 begin;
+ fg_vec2 end;
+ rect_vert(begin, i, v1, v2);
+ rect_vert(end, (i + 1) % 4, v1, v2);
+ fg_draw_gui_line(context, &style_, begin, end);
+ }
+ }
}
void
@@ 106,6 175,22 @@ fg_draw_gui_circle(
return;
prim.flags = prim.flags | (1 << 24);
push_rect(context, prim);
+ // TODO: Border
+}
+
+void
+fg_draw_gui_line(
+ struct fg_gui_context* context,
+ struct fg_gui_style const* style,
+ fg_vec2 const begin,
+ fg_vec2 const end) {
+ // Make sure the prim buffer has enough memory before doing anything
+ assert(context->lines.size_front + context->lines.size_back
+ < context->lines.capacity);
+ struct fg_gui_line prim;
+ if (!make_gui_line(context, style, begin, end, &prim))
+ return;
+ push_line(context, prim);
}
#undef CLIP_MASK
M src/gui_opengl_46.c => src/gui_opengl_46.c +97 -8
@@ 31,6 31,7 @@ struct opengl_resources {
GLuint vert_array; // Dummy. A VAO is necessary to call glDrawArrays.
GLuint clips;
struct opengl_prim rects;
+ struct opengl_prim lines;
};
static struct opengl_resources resources_g;
@@ 147,7 148,47 @@ load_rect_program(struct fg_allocator const* alloc) {
static GLuint
load_line_program(struct fg_allocator const* alloc) {
- return 0; // TODO
+ // Pre-compiled SPIR-V from glslangValidator. See meson.build.
+ static uint32_t const vert_spirv[] = {
+ #include "gui_line.vert.glsl.spv"
+ };
+ // TODO
+ // static uint32_t const tess_spirv[] = {
+ // #include "gui_line.tess.glsl.spv"
+ // };
+ static uint32_t const frag_spirv[] = {
+ #include "gui_line.frag.glsl.spv"
+ };
+
+ GLuint vert_shader = load_shader(
+ vert_spirv, sizeof(vert_spirv),
+ GL_VERTEX_SHADER,
+ alloc);
+ // GLuint tess_shader = load_shader(
+ // tess_spirv, sizeof(tess_spirv),
+ // GL_TESS_CONTROL_SHADER,
+ // alloc);
+ GLuint frag_shader = load_shader(
+ frag_spirv, sizeof(frag_spirv),
+ GL_FRAGMENT_SHADER,
+ alloc);
+
+ GLuint program = glCreateProgram();
+ glAttachShader(program, vert_shader);
+ glAttachShader(program, frag_shader);
+ glLinkProgram(program);
+
+ struct shader_result result = check_program(program, alloc);
+ if (!result.ok) {
+ fprintf(stderr, "Error linking program:\n%s", result.log);
+ alloc->free(result.log);
+ }
+
+ glDetachShader(program, vert_shader);
+ glDetachShader(program, frag_shader);
+ glDeleteShader(vert_shader);
+ glDeleteShader(frag_shader);
+ return program;
}
static inline void
@@ 203,9 244,32 @@ draw_rects(struct fg_gui_context* context) {
glDrawArrays(GL_TRIANGLES, 0, count * 6);
}
+static void
+draw_lines(struct fg_gui_context* context) {
+ int const count = context->lines.size_front + context->lines.size_back;
+ if (count <= 0)
+ return;
+
+ GLuint const buffers[] = {
+ resources_g.clips,
+ resources_g.lines.prims,
+ };
+ glBindBuffersBase(
+ GL_SHADER_STORAGE_BUFFER,
+ 0, sizeof(buffers) / sizeof(GLuint),
+ &buffers[0]);
+
+ glBindBufferBase(GL_UNIFORM_BUFFER, 0, resources_g.lines.uniform);
+
+ glUseProgram(resources_g.lines.program);
+ // Assumes a dummy vertex array has been bound
+ glDrawArrays(GL_LINES, 0, count * 2);
+}
+
void
fg_init_gui_opengl_46(struct fg_gui_context* context) {
resources_g.rects.program = load_rect_program(&context->alloc);
+ resources_g.lines.program = load_line_program(&context->alloc);
glCreateVertexArrays(1, &resources_g.vert_array);
@@ 215,6 279,7 @@ fg_init_gui_opengl_46(struct fg_gui_context* context) {
sizeof(float) * 4 * context->clips.capacity,
NULL, GL_STREAM_DRAW);
+ // Rects
glCreateBuffers(1, &resources_g.rects.uniform);
glNamedBufferData(
resources_g.rects.uniform,
@@ 226,14 291,30 @@ fg_init_gui_opengl_46(struct fg_gui_context* context) {
resources_g.rects.prims,
sizeof(struct fg_gui_rect) * context->rects.capacity,
NULL, GL_STREAM_DRAW);
+
+ // Lines
+ glCreateBuffers(1, &resources_g.lines.uniform);
+ glNamedBufferData(
+ resources_g.lines.uniform,
+ sizeof(struct shader_context),
+ NULL, GL_STREAM_DRAW);
+
+ glCreateBuffers(1, &resources_g.lines.prims);
+ glNamedBufferData(
+ resources_g.lines.prims,
+ sizeof(struct fg_gui_line) * context->lines.capacity,
+ NULL, GL_STREAM_DRAW);
}
void
fg_term_gui_opengl_46(void) {
glDeleteProgram(resources_g.rects.program);
+ glDeleteProgram(resources_g.lines.program);
glDeleteBuffers(1, &resources_g.clips);
glDeleteBuffers(1, &resources_g.rects.uniform);
glDeleteBuffers(1, &resources_g.rects.prims);
+ glDeleteBuffers(1, &resources_g.lines.uniform);
+ glDeleteBuffers(1, &resources_g.lines.prims);
}
void
@@ 250,17 331,18 @@ fg_commit_gui_opengl_46(struct fg_gui_context* context) {
0, sizeof(struct fg_gui_rect) * count,
(void const*)context->rects.prims);
}
+ {
+ int const count = context->lines.size_front + context->lines.size_back;
+ glNamedBufferSubData(
+ resources_g.lines.prims,
+ 0, sizeof(struct fg_gui_line) * count,
+ (void const*)context->lines.prims);
+ }
}
void
fg_draw_gui_opengl_46(struct fg_gui_context* context) {
- // Compress quad data into a single fg_gui_rect and expand vertex positions
- // and other vertex attributes in the vertex shader (which at this point is
- // a glorified "parallel for" loop). Doing this in OpenGL *may* circumvent
- // the post-transform cache, which could negatively affect performance.
- // https://www.khronos.org/opengl/wiki/Post_Transform_Cache
-
- // Bind dummy vertex array to pass glDrawArrays() checks
+ // Bind dummy vertex array to pass glDrawArrays checks
glBindVertexArray(resources_g.vert_array);
fg_vec2 viewport_size;
@@ 280,6 362,10 @@ fg_draw_gui_opengl_46(struct fg_gui_context* context) {
resources_g.rects.uniform,
0, sizeof(struct shader_context),
(void const*)&shd_context);
+ glNamedBufferSubData(
+ resources_g.lines.uniform,
+ 0, sizeof(struct shader_context),
+ (void const*)&shd_context);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);
@@ 290,6 376,9 @@ fg_draw_gui_opengl_46(struct fg_gui_context* context) {
draw_rects(context);
+ glEnable(GL_LINE_SMOOTH);
+ draw_lines(context);
+
// Depth mask *must* be re-enabled so that the next call to
// glClear(GL_DEPTH_BUFFER_BIT) is able to write to the depth buffer.
glDepthMask(GL_TRUE);