871 lines
26 KiB
Plaintext
871 lines
26 KiB
Plaintext
module idlist{Type};
|
|
|
|
// extends the List type to search elements that have a type.id property
|
|
// TODO: check that type has an id
|
|
|
|
import std::collections::list;
|
|
|
|
alias IdList = List{Type};
|
|
|
|
macro Type* IdList.get_from_name(&self, String name)
|
|
{
|
|
return self.get_from_id(name.hash());
|
|
}
|
|
|
|
macro Type* IdList.get_from_id(&self, id)
|
|
{
|
|
foreach(&s: self) {
|
|
if (s.id == id) {
|
|
return s;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
module sdlrenderer::ren;
|
|
|
|
// 2D renderer for ugui, based on SDL3 using the new GPU API
|
|
|
|
import std::io;
|
|
import std::core::mem;
|
|
import sdl3::sdl;
|
|
import idlist;
|
|
import ugui;
|
|
|
|
struct Shader {
|
|
sdl::GPUShader* frag;
|
|
sdl::GPUShader* vert;
|
|
ugui::Id id;
|
|
}
|
|
|
|
struct Pipeline {
|
|
sdl::GPUGraphicsPipeline* pipeline;
|
|
ugui::Id id;
|
|
}
|
|
|
|
struct Texture {
|
|
sdl::GPUTexture* texture;
|
|
sdl::GPUSampler* sampler;
|
|
ushort width, height;
|
|
ugui::Id id;
|
|
}
|
|
|
|
// The GPU buffers that contain quad info, the size is determined by MAX_QUAD_BATCH
|
|
const int MAX_QUAD_BATCH = 256;
|
|
struct QuadBuffer {
|
|
sdl::GPUBuffer* vert_buf;
|
|
sdl::GPUBuffer* idx_buf;
|
|
sdl::GPUTransferBuffer* transfer_buffer;
|
|
bool initialized;
|
|
int count;
|
|
int off; // the offset to draw from
|
|
}
|
|
|
|
alias ShaderList = IdList{Shader};
|
|
alias PipelineList = IdList{Pipeline};
|
|
alias TextureList = IdList{Texture};
|
|
|
|
struct Renderer {
|
|
sdl::Window* win;
|
|
sdl::GPUDevice* gpu;
|
|
sdl::GPURenderPass* render_pass;
|
|
sdl::GPUTexture* swapchain_texture;
|
|
sdl::GPUCommandBuffer* render_cmdbuf;
|
|
|
|
QuadBuffer quad_buffer;
|
|
ShaderList shaders;
|
|
PipelineList pipelines;
|
|
TextureList textures;
|
|
|
|
Id sprite_atlas_id;
|
|
Id font_atlas_id;
|
|
}
|
|
|
|
// how each vertex is represented in the gpu
|
|
struct Vertex @packed {
|
|
struct pos {
|
|
short x, y;
|
|
}
|
|
struct uv {
|
|
short u, v;
|
|
}
|
|
struct col { // FIXME: this is shit
|
|
union {
|
|
char r, g, b, a;
|
|
char[4] arr;
|
|
uint u;
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Quad @packed {
|
|
struct vertices {
|
|
Vertex v1,v2,v3,v4;
|
|
}
|
|
struct indices {
|
|
short i1,i2,i3,i4,i5,i6;
|
|
}
|
|
}
|
|
|
|
struct ViewsizeUniform @align(16) {
|
|
int w, h;
|
|
}
|
|
|
|
const int DEBUG = 1;
|
|
const bool CYCLE = true;
|
|
|
|
fn void Renderer.init(&self, ZString title, uint width, uint height)
|
|
{
|
|
// set wayland hint automagically
|
|
$if DEBUG == 0:
|
|
bool has_wayland = false;
|
|
for (int i = 0; i < sdl::get_num_video_drivers(); i++) {
|
|
ZString driver = sdl::get_video_driver(i);
|
|
if (driver.str_view() == "wayland") {
|
|
has_wayland = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (has_wayland) {
|
|
sdl::set_hint(sdl::HINT_VIDEO_DRIVER, "wayland");
|
|
}
|
|
$else
|
|
// in debug mode set the video driver to X11 because renderdoc
|
|
// doesn't support debugging in wayland yet.
|
|
sdl::set_hint(sdl::HINT_VIDEO_DRIVER, "x11");
|
|
sdl::set_hint(sdl::HINT_RENDER_GPU_DEBUG, "1");
|
|
$endif
|
|
|
|
// init subsystems
|
|
if (!sdl::init(INIT_VIDEO)) {
|
|
unreachable("sdl error: %s", sdl::get_error());
|
|
}
|
|
|
|
// create the window
|
|
self.win = sdl::create_window(title, width, height, WINDOW_RESIZABLE|WINDOW_VULKAN);
|
|
if (self.win == null) {
|
|
unreachable("sdl error: %s", sdl::get_error());
|
|
}
|
|
|
|
// get the gpu device handle
|
|
self.gpu = sdl::create_gpu_device(GPU_SHADERFORMAT_SPIRV, true, "vulkan");
|
|
if (self.gpu == null) {
|
|
unreachable("failed to create gpu device: %s", sdl::get_error());
|
|
}
|
|
|
|
if (!sdl::claim_window_for_gpu_device(self.gpu, self.win)) {
|
|
unreachable("failed to claim window for use with gpu: %s", sdl::get_error());
|
|
}
|
|
|
|
//
|
|
// initialize the quad buffer
|
|
// ==========================
|
|
self.quad_buffer.vert_buf = sdl::create_gpu_buffer(self.gpu,
|
|
&&(GPUBufferCreateInfo){.usage = GPU_BUFFERUSAGE_VERTEX, .size = Quad.vertices.sizeof * MAX_QUAD_BATCH}
|
|
);
|
|
if (self.quad_buffer.vert_buf == null) {
|
|
unreachable("failed to initialize quad buffer (vertex): %s", sdl::get_error());
|
|
}
|
|
|
|
self.quad_buffer.idx_buf = sdl::create_gpu_buffer(self.gpu,
|
|
&&(GPUBufferCreateInfo){.usage = GPU_BUFFERUSAGE_INDEX, .size = Quad.indices.sizeof * MAX_QUAD_BATCH}
|
|
);
|
|
if (self.quad_buffer.idx_buf == null) {
|
|
unreachable("failed to initialize quad buffer (index): %s", sdl::get_error());
|
|
}
|
|
|
|
self.quad_buffer.transfer_buffer = sdl::create_gpu_transfer_buffer(self.gpu,
|
|
&&(GPUTransferBufferCreateInfo){.usage = GPU_TRANSFERBUFFERUSAGE_UPLOAD, .size = Quad.sizeof}
|
|
);
|
|
if (self.quad_buffer.transfer_buffer == null) {
|
|
unreachable("failed to create gpu transfer buffer: %s", sdl::get_error());
|
|
}
|
|
|
|
self.quad_buffer.initialized = true;
|
|
}
|
|
|
|
fn void Renderer.free(&self)
|
|
{
|
|
foreach (&s: self.shaders) {
|
|
sdl::release_gpu_shader(self.gpu, s.frag);
|
|
sdl::release_gpu_shader(self.gpu, s.vert);
|
|
}
|
|
self.shaders.free();
|
|
|
|
foreach (&p: self.pipelines) {
|
|
sdl::release_gpu_graphics_pipeline(self.gpu, p.pipeline);
|
|
}
|
|
self.pipelines.free();
|
|
|
|
// FIXME: release the quad buffer
|
|
|
|
sdl::release_window_from_gpu_device(self.gpu, self.win);
|
|
sdl::destroy_gpu_device(self.gpu);
|
|
sdl::destroy_window(self.win);
|
|
sdl::quit();
|
|
}
|
|
|
|
fn void Renderer.resize_window(&self, uint width, uint height)
|
|
{
|
|
sdl::set_window_size(self.win, width, height);
|
|
}
|
|
|
|
fn void Renderer.get_window_size(&self, int* width, int* height)
|
|
{
|
|
sdl::get_window_size_in_pixels(self.win, width, height);
|
|
}
|
|
|
|
|
|
// Both the vertex shader and fragment shader have an implicit uniform buffer at binding 0 that
|
|
// contains the viewport size. It is populated automatically at every begin_render() call
|
|
fn void Renderer.load_spirv_shader_from_mem(&self, String name, char[] vert_code, char[] frag_code, uint textures, uint uniforms)
|
|
{
|
|
Shader s;
|
|
s.id = name.hash();
|
|
|
|
if (vert_code.len == 0 || frag_code.len == 0) {
|
|
unreachable("vertex shader and fragment shader cannot be empty");
|
|
}
|
|
|
|
if (vert_code.len > 0) {
|
|
// FIXME: these should be passed by parameter and/or automatically determined by parsing
|
|
// the shader code
|
|
GPUShaderCreateInfo shader_info = {
|
|
.code = vert_code.ptr,
|
|
.code_size = vert_code.len,
|
|
.entrypoint = "main",
|
|
.format = GPU_SHADERFORMAT_SPIRV,
|
|
.stage = GPU_SHADERSTAGE_VERTEX,
|
|
.num_samplers = 0,
|
|
.num_uniform_buffers = 1+uniforms,
|
|
.num_storage_buffers = 0,
|
|
.num_storage_textures = 0
|
|
};
|
|
|
|
s.vert = sdl::create_gpu_shader(self.gpu, &shader_info);
|
|
if (s.vert == null) {
|
|
unreachable("failed to create gpu vertex shader: %s", sdl::get_error());
|
|
}
|
|
}
|
|
|
|
if (frag_code.len > 0) {
|
|
// FIXME: these should be passed by parameter and/or automatically determined by parsing
|
|
// the shader code
|
|
GPUShaderCreateInfo shader_info = {
|
|
.code = frag_code.ptr,
|
|
.code_size = frag_code.len,
|
|
.entrypoint = "main",
|
|
.format = GPU_SHADERFORMAT_SPIRV,
|
|
.stage = GPU_SHADERSTAGE_FRAGMENT,
|
|
.num_samplers = textures,
|
|
.num_uniform_buffers = 1,
|
|
.num_storage_buffers = 0,
|
|
.num_storage_textures = 0
|
|
};
|
|
|
|
s.frag = sdl::create_gpu_shader(self.gpu, &shader_info);
|
|
if (s.frag == null) {
|
|
unreachable("failed to create gpu fragment shader: %s", sdl::get_error());
|
|
}
|
|
}
|
|
|
|
// push the shader into the list
|
|
self.shaders.push(s);
|
|
}
|
|
|
|
fn void Renderer.load_spirv_shader_from_file(&self, String name, String vert_path, String frag_path, uint textures, uint uniforms)
|
|
{
|
|
if (vert_path == "" || frag_path == "") {
|
|
unreachable("need both a vertex shader and fragment shader path");
|
|
}
|
|
|
|
char[] vert_code;
|
|
char[] frag_code;
|
|
|
|
// create vertex shader
|
|
vert_code = mem::new_array(char, file::get_size(vert_path)!!+1);
|
|
file::load_buffer(vert_path, vert_code)!!;
|
|
defer mem::free(vert_code);
|
|
|
|
// create fragment shader
|
|
frag_code = mem::new_array(char, file::get_size(frag_path)!!+1);
|
|
file::load_buffer(frag_path, frag_code)!!;
|
|
defer mem::free(frag_code);
|
|
|
|
self.load_spirv_shader_from_mem(name, vert_code, frag_code, textures, uniforms);
|
|
}
|
|
|
|
// this describes what we want to draw, since for drawing different things we have to change
|
|
// the GPUPrimitiveType and GPURasterizerState for the pipeline.
|
|
enum PipelineType : (GPUPrimitiveType primitive_type, GPURasterizerState raster_state) {
|
|
RECT = {GPU_PRIMITIVETYPE_TRIANGLELIST, {.fill_mode = GPU_FILLMODE_FILL, .cull_mode = GPU_CULLMODE_NONE, .front_face = GPU_FRONTFACE_COUNTER_CLOCKWISE}},
|
|
SPRITE = {GPU_PRIMITIVETYPE_TRIANGLELIST, {.fill_mode = GPU_FILLMODE_FILL, .cull_mode = GPU_CULLMODE_NONE, .front_face = GPU_FRONTFACE_COUNTER_CLOCKWISE}},
|
|
LINE = {GPU_PRIMITIVETYPE_LINELIST, {.fill_mode = GPU_FILLMODE_LINE, .cull_mode = GPU_CULLMODE_NONE, .front_face = GPU_FRONTFACE_COUNTER_CLOCKWISE}},
|
|
}
|
|
|
|
// create a graphics pipeline to draw to the window using a set of vertex/fragment shaders
|
|
// the pipeline is pushed into the renderer's pipeline list and it will have the same id as
|
|
// the shader set.
|
|
fn void Renderer.create_pipeline(&self, String shader_name, PipelineType type)
|
|
{
|
|
Shader *s = self.shaders.get_from_name(shader_name);
|
|
if (s == null) {
|
|
unreachable("error in creating pipeline: no shader named %s", shader_name);
|
|
}
|
|
|
|
GPUGraphicsPipelineCreateInfo ci = {
|
|
.vertex_shader = s.vert,
|
|
.fragment_shader = s.frag,
|
|
// This structure specifies how the vertex buffer looks in memory, what it contains
|
|
// and what is passed where to the gpu. Each vertex has three attributes, position,
|
|
// color and uv coordinates. Since this is a 2D pixel-based renderer the position
|
|
// is represented by two floats, the color as 32 bit rgba and the uv also as intgers.
|
|
.vertex_input_state = {
|
|
// the description of each vertex buffer, for now I use only one buffer
|
|
.vertex_buffer_descriptions = (GPUVertexBufferDescription[]){{
|
|
.slot = 0,
|
|
.pitch = Vertex.sizeof,
|
|
.input_rate = GPU_VERTEXINPUTRATE_VERTEX,
|
|
.instance_step_rate = 0,
|
|
}},
|
|
.num_vertex_buffers = 1,
|
|
// the description of each vertex, each vertex has three properties
|
|
.vertex_attributes = (GPUVertexAttribute[]){
|
|
{ // at location zero there is the position of the vertex
|
|
.location = 0,
|
|
.buffer_slot = 0, // only one buffer so always slot zero
|
|
.format = GPU_VERTEXELEMENTFORMAT_SHORT2,
|
|
.offset = Vertex.pos.offsetof,
|
|
},
|
|
{ // at location one there are the uv coordinates
|
|
.location = 1,
|
|
.buffer_slot = 0,
|
|
.format = GPU_VERTEXELEMENTFORMAT_SHORT2,
|
|
.offset = Vertex.uv.offsetof,
|
|
},
|
|
{ // at location two there is the color
|
|
.location = 2,
|
|
.buffer_slot = 0,
|
|
.format = GPU_VERTEXELEMENTFORMAT_UBYTE4, // 4x8bit unsigned rgba format
|
|
.offset = Vertex.col.offsetof,
|
|
}
|
|
},
|
|
.num_vertex_attributes = 3,
|
|
},
|
|
// the pipeline's primitive type and rasterizer state differs based on what needs to
|
|
// be drawn
|
|
.primitive_type = type.primitive_type,
|
|
.rasterizer_state = type.raster_state,
|
|
.multisample_state = {}, // no multisampling, all zeroes
|
|
.depth_stencil_state = {}, // no stencil test, all zeroes
|
|
.target_info = { // the target (texture) description
|
|
.color_target_descriptions = (GPUColorTargetDescription[]){{
|
|
// rendering happens to the window, so get it's format
|
|
.format = sdl::get_gpu_swapchain_texture_format(self.gpu, self.win),
|
|
.blend_state = {
|
|
// alpha blending on everything
|
|
// https://en.wikipedia.org/wiki/Alpha_compositing
|
|
.src_color_blendfactor = GPU_BLENDFACTOR_SRC_ALPHA,
|
|
.dst_color_blendfactor = GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA,
|
|
.color_blend_op = GPU_BLENDOP_ADD,
|
|
.src_alpha_blendfactor = GPU_BLENDFACTOR_SRC_ALPHA,
|
|
.dst_alpha_blendfactor = GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA,
|
|
.alpha_blend_op = GPU_BLENDOP_ADD,
|
|
.enable_blend = true,
|
|
// color write mask is not enabled so all rgba channels are written to
|
|
},
|
|
}},
|
|
.num_color_targets = 1,
|
|
.depth_stencil_format = {}, // FIXME: no stencil, no depth buffering
|
|
.has_depth_stencil_target = false,
|
|
},
|
|
};
|
|
|
|
// create the pipeline and add it to the pipeline list
|
|
Pipeline p = {
|
|
.id = s.id,
|
|
.pipeline = sdl::create_gpu_graphics_pipeline(self.gpu, &ci),
|
|
};
|
|
|
|
if (p.pipeline == null) {
|
|
unreachable("failed to create pipeline (shaders: %s, type: %s): %s", shader_name, type.nameof, sdl::get_error());
|
|
}
|
|
|
|
self.pipelines.push(p);
|
|
}
|
|
|
|
// NOTE: with TEXTUREUSAGE_SAMPLER the texture format cannot be intger _UINT so it has to be nermalized
|
|
enum TextureType : (GPUTextureFormat format) {
|
|
FULL_COLOR = GPU_TEXTUREFORMAT_R8G8B8A8_UNORM,
|
|
JUST_ALPHA = GPU_TEXTUREFORMAT_R8_UNORM
|
|
}
|
|
|
|
macro void Renderer.new_texture(&self, name_or_id, TextureType type, char[] pixels, uint width, uint height)
|
|
{
|
|
$switch $typeof(name_or_id):
|
|
$case usz: return self.new_texture_by_id(id, type, pixels, width, height);
|
|
$case String: return self.new_texture_by_id(name_or_id.hash(), type, pixels, width, height);
|
|
$default: unreachable("texture must have a name (String) or an id (usz)");
|
|
$endswitch
|
|
}
|
|
|
|
macro void Renderer.update_texture(&self, name_or_id, char[] pixels, uint width, uint height, uint x = 0, uint y = 0)
|
|
{
|
|
$switch $typeof(name_or_id):
|
|
$case usz: return self.update_texture_by_id(name_or_id, pixels, width, height, x, y);
|
|
$case String: return self.update_texture_by_id(name_or_id.hash(), pixels, width, height, x, y);
|
|
$default: unreachable("texture must have a name (String) or an id (usz)");
|
|
$endswitch
|
|
}
|
|
|
|
|
|
// create a new gpu texture from a pixel buffer, the format has to be specified
|
|
// the new texture s given an id and pushed into a texture list
|
|
fn void Renderer.new_texture_by_id(&self, Id id, TextureType type, char[] pixels, uint width, uint height)
|
|
{
|
|
// the texture description
|
|
GPUTextureCreateInfo tci = {
|
|
.type = GPU_TEXTURETYPE_2D,
|
|
.format = type.format,
|
|
// all textures are used with samplers, which means read-only textures that contain data to be sampled
|
|
.usage = GPU_TEXTUREUSAGE_SAMPLER,
|
|
.width = width,
|
|
.height = height,
|
|
.layer_count_or_depth = 1,
|
|
.num_levels = 1, // no mip maps so just one level
|
|
// .sample_count not used since the texture is not a render target
|
|
};
|
|
|
|
GPUTexture* texture = sdl::create_gpu_texture(self.gpu, &tci);
|
|
if (texture == null) {
|
|
unreachable("failed to create texture (id: %s, type: %s): %s", id, type.nameof, sdl::get_error());
|
|
}
|
|
|
|
// the sampler description, how the texture should be sampled
|
|
GPUSamplerCreateInfo sci = {
|
|
.min_filter = GPU_FILTER_LINEAR, // linear interpolation for textures
|
|
.mag_filter = GPU_FILTER_LINEAR,
|
|
.mipmap_mode = GPU_SAMPLERMIPMAPMODE_NEAREST,
|
|
.address_mode_u = GPU_SAMPLERADDRESSMODE_REPEAT, // tiling textures
|
|
.address_mode_v = GPU_SAMPLERADDRESSMODE_REPEAT,
|
|
.address_mode_w = GPU_SAMPLERADDRESSMODE_REPEAT,
|
|
// everything else is not used and not needed
|
|
};
|
|
|
|
GPUSampler* sampler = sdl::create_gpu_sampler(self.gpu, &sci);
|
|
if (sampler == null) {
|
|
unreachable("failed to create sampler (texture id: %s, type: %s): %s", id, type.nameof, sdl::get_error());
|
|
}
|
|
|
|
Texture t = {
|
|
.id = id,
|
|
.texture = texture,
|
|
.sampler = sampler,
|
|
};
|
|
self.textures.push(t);
|
|
|
|
// upload the texture data
|
|
self.update_texture_by_id(id, pixels, width, height, 0, 0);
|
|
}
|
|
|
|
fn void Renderer.update_texture_by_id(&self, Id id, char[] pixels, uint width, uint height, uint x, uint y)
|
|
{
|
|
Texture* t = self.textures.get_from_id(id);
|
|
if (t == null || t.texture == null) {
|
|
unreachable("failed updating texture: no texture with id %s", id);
|
|
}
|
|
GPUTexture* texture = t.texture;
|
|
|
|
// FIXME: do a better job at validating the copy
|
|
if (x > t.width || y > t.height) {
|
|
unreachable("failed updating texture: attempting to copy outside of the texture region");
|
|
}
|
|
|
|
// upload image data
|
|
GPUCommandBuffer* cmdbuf = sdl::acquire_gpu_command_buffer(self.gpu);
|
|
if (cmdbuf == null) {
|
|
unreachable("failed to upload texture data at acquiring command buffer: %s", sdl::get_error());
|
|
}
|
|
GPUCopyPass* copypass = sdl::begin_gpu_copy_pass(cmdbuf);
|
|
if (copypass == null) {
|
|
unreachable("failed to upload texture data at beginning copy pass: %s", sdl::get_error());
|
|
}
|
|
|
|
GPUTransferBuffer* buf = sdl::create_gpu_transfer_buffer(self.gpu,
|
|
&&(GPUTransferBufferCreateInfo){.usage = GPU_TRANSFERBUFFERUSAGE_UPLOAD, .size = pixels.len}
|
|
);
|
|
if (buf == null) {
|
|
unreachable("failed to upload texture data at creating the transfer buffer: %s", sdl::get_error());
|
|
}
|
|
|
|
char* gpu_mem = (char*)sdl::map_gpu_transfer_buffer(self.gpu, buf, CYCLE);
|
|
if (gpu_mem == null) {
|
|
unreachable("failed to upload texture data at mapping the transfer buffer: %s", sdl::get_error());
|
|
}
|
|
// copy the data to the driver's memory
|
|
gpu_mem[:pixels.len] = pixels[..];
|
|
sdl::unmap_gpu_transfer_buffer(self.gpu, buf);
|
|
|
|
// upload the data to gpu memory
|
|
sdl::upload_to_gpu_texture(copypass,
|
|
&&(GPUTextureTransferInfo){.transfer_buffer = buf, .offset = 0},
|
|
&&(GPUTextureRegion){.texture = texture, .x = x, .y = y, .w = width, .h = height, .d = 1},
|
|
false
|
|
);
|
|
|
|
sdl::end_gpu_copy_pass(copypass);
|
|
if (!sdl::submit_gpu_command_buffer(cmdbuf)) {
|
|
unreachable("failed to upload texture data at command buffer submission: %s", sdl::get_error());
|
|
}
|
|
sdl::release_gpu_transfer_buffer(self.gpu, buf);
|
|
}
|
|
|
|
|
|
fn bool Renderer.push_sprite(&self, short x, short y, short w, short h, short u, short v, uint color = 0xffffffff)
|
|
{
|
|
Quad quad;
|
|
/* v1 v4
|
|
* +-------------+
|
|
* | _/|
|
|
* | _/ |
|
|
* | 1 _/ |
|
|
* | _/ |
|
|
* | _/ |
|
|
* | _/ 2 |
|
|
* |/ |
|
|
* +-------------+
|
|
* v2 v3
|
|
*/
|
|
quad.vertices.v1 = {.pos = {.x = x, .y = y}, .uv = {.u = u, .v = v}, .col.u = color};
|
|
quad.vertices.v2 = {.pos = {.x = x, .y = y+h}, .uv = {.u = u, .v = v+h}, .col.u = color};
|
|
quad.vertices.v3 = {.pos = {.x = x+w, .y = y+h}, .uv = {.u = u+w, .v = v+h}, .col.u = color};
|
|
quad.vertices.v4 = {.pos = {.x = x+w, .y = y}, .uv = {.u = u+w, .v = v}, .col.u = color};
|
|
// triangle 1 indices
|
|
quad.indices.i1 = 0; // v1
|
|
quad.indices.i2 = 1; // v2
|
|
quad.indices.i3 = 3; // v4
|
|
// triangle 2 indices
|
|
quad.indices.i4 = 1; // v2
|
|
quad.indices.i5 = 2; // v3
|
|
quad.indices.i6 = 3; // v4
|
|
|
|
return self.upload_quad(&quad);
|
|
}
|
|
|
|
// Push a quad into the quad buffer, return true on success and false on failure
|
|
fn bool Renderer.push_quad(&self, short x, short y, short w, short h, uint color, ushort radius = 0)
|
|
{
|
|
Quad quad;
|
|
/* v1 v4
|
|
* +-------------+
|
|
* | _/|
|
|
* | _/ |
|
|
* | 1 _/ |
|
|
* | _/ |
|
|
* | _/ |
|
|
* | _/ 2 |
|
|
* |/ |
|
|
* +-------------+
|
|
* v2 v3
|
|
*/
|
|
// the wanted radius is pushed into the uv coordinates, the vertex shader then extracts the absolute value
|
|
// and passes it to the fragment shader, then it uses the sign to give the fragment shader local coordinates
|
|
// into the quad.
|
|
quad.vertices.v1 = {.pos = {.x = x, .y = y}, .uv = {.u = -radius, .v = +radius}, .col.u = color};
|
|
quad.vertices.v2 = {.pos = {.x = x, .y = y+h}, .uv = {.u = -radius, .v = -radius}, .col.u = color};
|
|
quad.vertices.v3 = {.pos = {.x = x+w, .y = y+h}, .uv = {.u = +radius, .v = -radius}, .col.u = color};
|
|
quad.vertices.v4 = {.pos = {.x = x+w, .y = y}, .uv = {.u = +radius, .v = +radius}, .col.u = color};
|
|
// triangle 1 indices
|
|
quad.indices.i1 = 0; // v1
|
|
quad.indices.i2 = 1; // v2
|
|
quad.indices.i3 = 3; // v4
|
|
// triangle 2 indices
|
|
quad.indices.i4 = 1; // v2
|
|
quad.indices.i5 = 2; // v3
|
|
quad.indices.i6 = 3; // v4
|
|
|
|
return self.upload_quad(&quad);
|
|
}
|
|
|
|
fn bool Renderer.upload_quad(&self, Quad* source_quad)
|
|
{
|
|
if (self.quad_buffer.count >= MAX_QUAD_BATCH || source_quad == null) {
|
|
return false;
|
|
}
|
|
QuadBuffer* qb = &self.quad_buffer;
|
|
|
|
// upload the quad data to the gpu
|
|
if (qb.initialized == false) {
|
|
unreachable("quad buffer not initialized");
|
|
}
|
|
|
|
Quad* quad = (Quad*)sdl::map_gpu_transfer_buffer(self.gpu, qb.transfer_buffer, CYCLE);
|
|
if (quad == null) {
|
|
unreachable("failed to map gpu transfer buffer: %s", sdl::get_error());
|
|
}
|
|
|
|
*quad = *source_quad;
|
|
|
|
sdl::unmap_gpu_transfer_buffer(self.gpu, qb.transfer_buffer);
|
|
|
|
GPUCommandBuffer* cmd = sdl::acquire_gpu_command_buffer(self.gpu);
|
|
if (cmd == null) {
|
|
unreachable("failed to upload quad at acquiring command buffer: %s", sdl::get_error());
|
|
}
|
|
GPUCopyPass* cpy = sdl::begin_gpu_copy_pass(cmd);
|
|
|
|
// upload vertices
|
|
sdl::upload_to_gpu_buffer(cpy,
|
|
&&(GPUTransferBufferLocation){.transfer_buffer = qb.transfer_buffer, .offset = Quad.vertices.offsetof},
|
|
&&(GPUBufferRegion){.buffer = qb.vert_buf, .offset = qb.count * Quad.vertices.sizeof, .size = Quad.vertices.sizeof},
|
|
false
|
|
);
|
|
// upload indices
|
|
sdl::upload_to_gpu_buffer(cpy,
|
|
&&(GPUTransferBufferLocation){.transfer_buffer = qb.transfer_buffer, .offset = Quad.indices.offsetof},
|
|
&&(GPUBufferRegion){.buffer = qb.idx_buf, .offset = qb.count * Quad.indices.sizeof, .size = Quad.indices.sizeof},
|
|
false
|
|
);
|
|
|
|
sdl::end_gpu_copy_pass(cpy);
|
|
if (!sdl::submit_gpu_command_buffer(cmd)) {
|
|
unreachable("failed to upload quads at submit command buffer: %s", sdl::get_error());
|
|
}
|
|
|
|
qb.count++;
|
|
|
|
return true;
|
|
}
|
|
|
|
// draw all quads in the quad buffer, since uniforms are per-drawcall it makes no sense
|
|
// to draw them one a the time
|
|
fn void Renderer.draw_quads(&self)
|
|
{
|
|
QuadBuffer* qb = &self.quad_buffer;
|
|
|
|
if (qb.off == qb.count) return;
|
|
|
|
sdl::bind_gpu_vertex_buffers(self.render_pass, 0, (GPUBufferBinding[]){{.buffer = qb.vert_buf, .offset = qb.off*Quad.vertices.sizeof}}, 1);
|
|
sdl::bind_gpu_index_buffer(self.render_pass, &&(GPUBufferBinding){.buffer = qb.idx_buf, .offset = qb.off*Quad.indices.sizeof}, GPU_INDEXELEMENTSIZE_16BIT);
|
|
|
|
// we need instancing to not do this
|
|
for (int i = 0; i < qb.count - qb.off; i++) {
|
|
sdl::draw_gpu_indexed_primitives(self.render_pass, 6, 1, i*6, i*4, 0);
|
|
}
|
|
|
|
qb.off = qb.count;
|
|
}
|
|
|
|
fn void Renderer.reset_quads(&self)
|
|
{
|
|
self.quad_buffer.count = 0;
|
|
self.quad_buffer.off = 0;
|
|
}
|
|
|
|
|
|
fn void Renderer.begin_render(&self, bool clear_screen)
|
|
{
|
|
self.render_cmdbuf = sdl::acquire_gpu_command_buffer(self.gpu);
|
|
sdl::wait_and_acquire_gpu_swapchain_texture(self.render_cmdbuf, self.win, &self.swapchain_texture, null, null);
|
|
|
|
// push the window size as a uniform
|
|
// TODO: maybe make this configurable and/or add more things
|
|
ViewsizeUniform v;
|
|
self.get_window_size(&v.w, &v.h);
|
|
sdl::push_gpu_vertex_uniform_data(self.render_cmdbuf, 0, &v, ViewsizeUniform.sizeof);
|
|
sdl::push_gpu_fragment_uniform_data(self.render_cmdbuf, 0, &v, ViewsizeUniform.sizeof);
|
|
|
|
if (clear_screen) {
|
|
GPURenderPass* pass = sdl::begin_gpu_render_pass(self.render_cmdbuf,
|
|
&&(GPUColorTargetInfo){
|
|
.texture = self.swapchain_texture,
|
|
.mip_level = 0,
|
|
.layer_or_depth_plane = 0,
|
|
.clear_color = {.r = 1.0, .g = 0.0, .b = 1.0, .a = 1.0},
|
|
.load_op = GPU_LOADOP_CLEAR, // clear the screen at the start of the render pass
|
|
.store_op = GPU_STOREOP_STORE,
|
|
.resolve_texture = null,
|
|
.resolve_mip_level = 0,
|
|
.resolve_layer = 0,
|
|
.cycle = false,
|
|
.cycle_resolve_texture = false
|
|
},
|
|
1,
|
|
null // huh
|
|
);
|
|
if (pass == null) {
|
|
unreachable("render pass creation went wrong: %s", sdl::get_error());
|
|
}
|
|
|
|
sdl::end_gpu_render_pass(pass);
|
|
}
|
|
}
|
|
|
|
fn void Renderer.end_render(&self)
|
|
{
|
|
sdl::submit_gpu_command_buffer(self.render_cmdbuf);
|
|
self.reset_quads();
|
|
}
|
|
|
|
fn void Renderer.start_render_pass(&self, String pipeline_name)
|
|
{
|
|
self.render_pass = sdl::begin_gpu_render_pass(self.render_cmdbuf,
|
|
&&(GPUColorTargetInfo){
|
|
.texture = self.swapchain_texture,
|
|
.mip_level = 0,
|
|
.layer_or_depth_plane = 0,
|
|
.clear_color = {.r = 0.0, .g = 0.0, .b = 0.0, .a = 1.0},
|
|
.load_op = GPU_LOADOP_DONT_CARE,
|
|
.store_op = GPU_STOREOP_STORE,
|
|
.resolve_texture = null,
|
|
.resolve_mip_level = 0,
|
|
.resolve_layer = 0,
|
|
.cycle = false,
|
|
.cycle_resolve_texture = false
|
|
},
|
|
1,
|
|
null // huh
|
|
);
|
|
|
|
if (self.render_pass == null) {
|
|
unreachable("render pass creation went wrong: %s", sdl::get_error());
|
|
}
|
|
|
|
sdl::GPUGraphicsPipeline* p;
|
|
p = self.pipelines.get_from_name(pipeline_name).pipeline;
|
|
if (p == null) {
|
|
unreachable("no pipeline");
|
|
}
|
|
|
|
sdl::bind_gpu_graphics_pipeline(self.render_pass, p);
|
|
}
|
|
|
|
fn void Renderer.end_render_pass(&self)
|
|
{
|
|
sdl::end_gpu_render_pass(self.render_pass);
|
|
}
|
|
|
|
fn void Renderer.bind_texture(&self, String texture_name)
|
|
{
|
|
ren::Texture* tx = self.textures.get_from_name(texture_name);
|
|
sdl::bind_gpu_fragment_samplers(self.render_pass, 0,
|
|
(GPUTextureSamplerBinding[]){{.texture = tx.texture, .sampler = tx.sampler}}, 1
|
|
);
|
|
}
|
|
|
|
fn void Renderer.set_scissor(&self, uint x, uint y, uint w, uint h)
|
|
{
|
|
sdl::set_gpu_scissor(self.render_pass, &&(sdl::Rect){x,y,w,h});
|
|
}
|
|
|
|
fn void Renderer.reset_scissor(&self)
|
|
{
|
|
int w, h;
|
|
sdl::get_window_size(self.win, &w, &h);
|
|
self.set_scissor(0, 0, w, h);
|
|
}
|
|
|
|
/// === NOTES ===
|
|
/* 1. The uniform data is per-render pass. So you can do:
|
|
* - push uniform
|
|
* - draw 1
|
|
* - draw 2
|
|
* But not:
|
|
* - push uniform
|
|
* - draw
|
|
* - push new uniform
|
|
* - draw
|
|
* And not even:
|
|
* - draw
|
|
* - push uniform
|
|
* - draw
|
|
*
|
|
* 2. The GPU buffers are read per-command-buffer and not per
|
|
* render pass. So I cannot override an element in the buffer
|
|
* before submitting the command buffer.
|
|
*/
|
|
/// === END NOTES ===
|
|
|
|
|
|
fn void Renderer.render_ugui(&self, CmdQueue* queue)
|
|
{
|
|
Cmd* last_command;
|
|
for (Cmd* cmd; (cmd = queue.dequeue() ?? null) != null;) {
|
|
if (last_command == null || last_command.type != cmd.type) {
|
|
self.end_command(last_command);
|
|
self.begin_command(cmd);
|
|
}
|
|
|
|
switch (cmd.type) {
|
|
case CMD_RECT:
|
|
CmdRect r = cmd.rect;
|
|
self.push_quad(r.rect.x, r.rect.y, r.rect.w, r.rect.h, r.color.to_uint(), r.radius);
|
|
case CMD_SPRITE:
|
|
// TODO: support hue in sprite
|
|
CmdSprite s = cmd.sprite;
|
|
self.push_sprite(s.rect.x, s.rect.y, s.texture_rect.w, s.texture_rect.h, s.texture_rect.x, s.texture_rect.y);
|
|
case CMD_UPDATE_ATLAS:
|
|
// TODO: verify the correct type
|
|
CmdUpdateAtlas u = cmd.update_atlas;
|
|
char[] pixels = u.raw_buffer[..u.width*u.height*u.bpp];
|
|
self.update_texture(u.id, pixels, u.width, u.height);
|
|
case CMD_SCISSOR: break; // FIXME: ugui sends a scissor event before any rect event, this cannot be done and needs different handling
|
|
ugui::Rect s = cmd.scissor.rect;
|
|
self.set_scissor(s.x, s.y, s.w, s.h);
|
|
default: unreachable("unknown command: %s", cmd.type);
|
|
}
|
|
|
|
last_command = cmd;
|
|
}
|
|
self.end_command(last_command);
|
|
}
|
|
|
|
fn void Renderer.begin_command(&self, Cmd* cmd)
|
|
{
|
|
if (cmd == null) return;
|
|
|
|
switch (cmd.type) {
|
|
case CMD_RECT:
|
|
self.start_render_pass("UGUI_PIPELINE_RECT");
|
|
case CMD_SPRITE:
|
|
// TODO: support multiple sprite and font atlases
|
|
CmdSprite s = cmd.sprite;
|
|
String pipeline;
|
|
String texture;
|
|
if (s.texture_id == self.sprite_atlas_id) {
|
|
switch (s.type) {
|
|
case SPRITE_NORMAL: pipeline = "UGUI_PIPELINE_SPRITE";
|
|
case SPRITE_SDF: pipeline = "UGUI_PIPELINE_SPRITE_SDF";
|
|
case SPRITE_MSDF: pipeline = "UGUI_PIPELINE_SPRITE_MSDF";
|
|
case SPRITE_ANIMATED: unreachable("animated sprtes are unsupported for now");
|
|
default: unreachable("unknown sprite type %s", s.type);
|
|
}
|
|
texture = "icons";
|
|
} else if (s.texture_id == self.font_atlas_id) {
|
|
pipeline = "UGUI_PIPELINE_FONT";
|
|
texture = "font1";
|
|
}
|
|
self.start_render_pass(pipeline);
|
|
self.bind_texture(texture);
|
|
case CMD_UPDATE_ATLAS: break;
|
|
case CMD_SCISSOR: break;
|
|
default: unreachable("unknown command: %s", cmd.type);
|
|
}
|
|
}
|
|
|
|
fn void Renderer.end_command(&self, Cmd* cmd)
|
|
{
|
|
if (cmd == null) return;
|
|
|
|
switch (cmd.type) {
|
|
case CMD_RECT: nextcase;
|
|
case CMD_SPRITE:
|
|
self.draw_quads();
|
|
self.end_render_pass();
|
|
case CMD_UPDATE_ATLAS: break;
|
|
case CMD_SCISSOR: break;
|
|
default: unreachable("unknown command: %s", cmd.type);
|
|
}
|
|
}
|