~rycwo/forge

e5de0236be043d41e133aa1bfcca8c2a71ed956f — Ryan Chan 4 months ago b80b667 master
Add support for basic line rendering

Line rendering is also used to draw straight borders for rect
primitives. border_width and corner_radius are still unsupported as
variable line width and arcs/curves have yet to be implemented.
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);