commit f47ad817f1bc4923e4be5efc58841624440bdaf3 Author: Patrick Cleavelin Date: Thu May 23 14:34:15 2024 -0500 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e916721 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +bin + diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..47f370a --- /dev/null +++ b/flake.lock @@ -0,0 +1,180 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "locked": { + "lastModified": 1659877975, + "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_3": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1705309234, + "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixgl": { + "inputs": { + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1713543440, + "narHash": "sha256-lnzZQYG0+EXl/6NkGpyIz+FEOc/DSEG57AP1VsdeNrM=", + "owner": "guibou", + "repo": "nixGL", + "rev": "310f8e49a149e4c9ea52f1adf70cdc768ec53f8a", + "type": "github" + }, + "original": { + "owner": "guibou", + "repo": "nixGL", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1660551188, + "narHash": "sha256-a1LARMMYQ8DPx1BgoI/UN4bXe12hhZkCNqdxNi6uS0g=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "441dc5d512153039f19ef198e662e4f3dbb9fd65", + "type": "github" + }, + "original": { + "owner": "nixos", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1715087517, + "narHash": "sha256-CLU5Tsg24Ke4+7sH8azHWXKd0CFd4mhLWfhYgUiDBpQ=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "b211b392b8486ee79df6cdfb1157ad2133427a29", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_3": { + "locked": { + "lastModified": 1706487304, + "narHash": "sha256-LE8lVX28MV2jWJsidW13D2qrHU/RUUONendL2Q/WlJg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "90f456026d284c22b3e3497be980b2e47d0b28ac", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixgl": "nixgl", + "nixpkgs": "nixpkgs_2", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "flake-utils": "flake-utils_3", + "nixpkgs": "nixpkgs_3" + }, + "locked": { + "lastModified": 1715221036, + "narHash": "sha256-81EKOdlmT/4hZpImRlvMVPgmCcJYZjwlWbJese/XqUw=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "5c4bc8a0a70093a31a12509c5653c147f2310bd2", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100755 index 0000000..5919680 --- /dev/null +++ b/flake.nix @@ -0,0 +1,80 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + nixgl.url = "github:guibou/nixGL"; + rust-overlay.url = "github:oxalica/rust-overlay"; + }; + + outputs = { self, nixpkgs, flake-utils, nixgl, rust-overlay, ... }: + flake-utils.lib.eachDefaultSystem (system: + let + overlays = [ nixgl.overlay (import rust-overlay) ]; + pkgs = import nixpkgs { + inherit system overlays; + }; + fixed-odin = pkgs.odin.overrideAttrs (finalAttrs: prevAttr: rec { + src = pkgs.fetchFromGitHub { + owner = "odin-lang"; + repo = "Odin"; + rev = "aab122ede8b04a9877e22c9013c0b020186bc9b4"; + # hash = "sha256-pxvU5veB1NEYPfer5roiLp/od2Pv4l1jJah0OHwb5yo="; + }; + LLVM_CONFIG = "${pkgs.llvmPackages_17.llvm.dev}/bin/llvm-config"; + nativeBuildInputs = with pkgs; prevAttr.nativeBuildInputs ++ [ libcxx libcxxabi ]; + postPatch = prevAttr.postPatch + '' + sed -i build_odin.sh \ + -e 's|CXXFLAGS="$CXXFLAGS $($LLVM_CONFIG --cxxflags --ldflags)"|CXXFLAGS="$CXXFLAGS $($LLVM_CONFIG --cxxflags --ldflags) -I ${pkgs.libiconv.outPath}/include/"|' \ + -e 's|LDFLAGS="$LDFLAGS -pthread -lm -lstdc++"|LDFLAGS="$LDFLAGS -pthread -lm -lstdc++ -L ${pkgs.libiconv.outPath}/lib/ -L ${pkgs.llvmPackages_17.libcxxabi.outPath}/lib/"|' + ''; + installPhase = '' + runHook preInstall + + mkdir -p $out/bin + cp odin $out/bin/odin + + mkdir -p $out/share + cp -r core $out/share/core + cp -r vendor $out/share/vendor + + wrapProgram $out/bin/odin \ + --set PATH ${pkgs.lib.makeBinPath (with pkgs; [ + coreutils + llvmPackages_17.bintools + llvmPackages_17.lld + llvmPackages_17.clang + ])} \ + --set-default ODIN_ROOT $out/share + + runHook postInstall + ''; + }); + in + { + devShell = pkgs.mkShell { + buildInputs = with pkgs; (if pkgs.system == "aarch64-darwin" || pkgs.system == "x86_64-darwin" then [ + odin + darwin.apple_sdk.frameworks.CoreData + darwin.apple_sdk.frameworks.Kernel + darwin.apple_sdk.frameworks.CoreVideo + darwin.apple_sdk.frameworks.GLUT + darwin.apple_sdk.frameworks.IOKit + darwin.apple_sdk.frameworks.OpenGL + darwin.apple_sdk.frameworks.Cocoa + ] else if pkgs.system == "x86_64-linux" then [ + pkg-config + binutils + odin + libGL + xorg.libX11 + xorg.libXi + xorg.xinput + xorg.libXcursor + xorg.libXrandr + xorg.libXinerama + pkgs.nixgl.nixGLIntel + ] else throw "unsupported system" ); + }; + } + ); +} diff --git a/justfile b/justfile new file mode 100644 index 0000000..90c1f3e --- /dev/null +++ b/justfile @@ -0,0 +1,27 @@ +alias b := build +alias r := run + +c_flags := if os() == "macos" { + "$(curl-config --libs) -framework Cocoa -framework QuartzCore -framework CoreImage -framework Metal -framework MetalKit -ObjC" +} else if os_family() == "unix" { + "$(curl-config --libs) -lEGL -lGLESv2 -lGL -lm -lwayland-client -lwayland-egl -lX11 -lXi -lXcursor -Wno-implicit-function-declaration" +} else { "" } + +build: build_gfx + odin build src/ -out:bin/chat_client -debug -lld -extra-linker-flags:"-framework Cocoa -framework QuartzCore -framework CoreImage -framework Metal -framework MetalKit" + +[macos] +build_gfx: transpile_shaders_metal + mkdir -p bin + cc -O0 -g -c -ObjC -Wall -Wextra -D_FONT_WIDTH=12 -D_FONT_HEIGHT=24 -DED_GFX_IMPLEMENTATION vendor/pcleavelin/gfx.h -o bin/libgfx.a + +[macos] +run: build + ./bin/chat_client + +[macos] +transpile_shaders_metal: + mkdir -p bin/transpiled_shaders + + xcrun -sdk macosx metal -o bin/transpiled_shaders/text_atlas.ir -c shaders/text_atlas.metal + xcrun -sdk macosx metallib -o bin/shaders.metallib bin/transpiled_shaders/text_atlas.ir diff --git a/shaders/text_atlas.metal b/shaders/text_atlas.metal new file mode 100644 index 0000000..2a7a078 --- /dev/null +++ b/shaders/text_atlas.metal @@ -0,0 +1,145 @@ +#include + +using namespace metal; + +struct VertexInput { + float2 position; + float2 tex_coord; +}; + +struct VertexOutput { + float4 position [[position]]; + float4 color; + float2 tex_coord; +}; + +struct Glyph { + float2 atlas_position; + float2 size; + float2 target_position; + float y_offset; + float _haha_alignment; + float4 color; +}; + +struct UniformParams { + float2 screen_size; +}; + +float4 to_device_position(float2 position, float2 size) { + return float4(((position / size) * 2.0) - float2(1.0), 1.0, 1.0); +} + +vertex VertexOutput +vs_main( + uint vertex_id [[vertex_id]], + uint glyph_id [[instance_id]], + constant VertexInput *vertices [[buffer(0)]], + constant Glyph *glyphs [[buffer(1)]], + constant UniformParams ¶ms [[buffer(2)]] +) +{ + VertexOutput out; + + Glyph glyph = glyphs[glyph_id]; + + float2 scaled_size = ((vertices[vertex_id].position + 1.0) / 2.0) * (glyph.size/2.0); + float2 scaled_size_2 = ((vertices[vertex_id].position + 1.0) / 2.0) * (glyph.size); + float2 glyph_pos = scaled_size + glyph.target_position + float2(0, glyph.y_offset/2.0+24); + + float4 device_position = to_device_position(glyph_pos, params.screen_size); + float2 atlas_position = (scaled_size_2 + glyph.atlas_position) / 512.0; + + device_position.y = -device_position.y; + + out.position = device_position; + out.color = glyph.color; + out.tex_coord = atlas_position; + + return out; +} + +fragment float4 fs_main(VertexOutput in [[stage_in]], + texture2d texture [[texture(0)]]) +{ + constexpr sampler texture_sampler (mag_filter::linear, min_filter::linear); + + float text_color = texture.sample(texture_sampler, in.tex_coord).r; + return float4(in.color.rgb, text_color); +} + +struct UiRect { + float4 position; + float4 size; + float4 border_size; + float4 color; +}; + +struct UiRectFragment { + float4 device_position [[position]]; + float2 position; + float2 size; + float2 border_size; + float2 screen_size; + float2 tex_coord; + float4 color; +}; + +vertex UiRectFragment +ui_rect_vs( + uint vertex_id [[vertex_id]], + uint rect_id [[instance_id]], + constant VertexInput *vertices [[buffer(0)]], + constant UiRect *rects [[buffer(1)]], + constant UniformParams ¶ms [[buffer(2)]] +) +{ + UiRect rect = rects[rect_id]; + + float2 scaled_size = ((vertices[vertex_id].position.xy + 1.0) / 2.0) * (rect.size.xy); + float2 rect_pos = scaled_size + rect.position.xy; + + float4 device_position = to_device_position(rect_pos, params.screen_size); + device_position.y = -device_position.y; + + return UiRectFragment { + device_position, + rect.position.xy, + rect.size.xy, + rect.border_size.xy, + params.screen_size, + vertices[vertex_id].tex_coord, + rect.color, + }; +} + +float rect_sdf( + float2 absolute_pixel_position, + float2 origin, + float2 size, + float corner_radius +) { + float2 half_size = size / 2; + float2 rect_center = origin + half_size; + + float2 pixel_position = abs(absolute_pixel_position - rect_center); + float2 shrunk_corner_position = half_size - corner_radius; + + float2 pixel_to_shrunk_corner = max(float2(0), pixel_position - shrunk_corner_position); + float distance_to_shrunk_corner = length(pixel_to_shrunk_corner); + float distance = distance_to_shrunk_corner - corner_radius; + + return distance; +} + +fragment float4 ui_rect_fs(UiRectFragment in [[stage_in]]) +{ + float2 pixel_pos = (in.device_position.xy + 1.0) / 2.0; + + float distance = rect_sdf(pixel_pos, in.position, in.size, in.border_size.x); + if (distance <= 0.0) { + return in.color; + } else { + return float4(0); + } +} diff --git a/shaders/text_atlas_fragment.glsl b/shaders/text_atlas_fragment.glsl new file mode 100644 index 0000000..de14d3a --- /dev/null +++ b/shaders/text_atlas_fragment.glsl @@ -0,0 +1,18 @@ +#version 440 core + +struct VertexOutput { + vec4 position; + vec2 tex_coord; +}; + + +layout(location = 0) in VertexOutput out_vertex; +layout(location = 1) uniform highp sampler2D atlas_texture; + +out vec4 color; + +void main() { + float text_color = texture(atlas_texture, out_vertex.tex_coord).r; + + color = vec4(text_color * vec3(1,1,1), text_color); +} diff --git a/shaders/text_atlas_vertex.glsl b/shaders/text_atlas_vertex.glsl new file mode 100644 index 0000000..f062f29 --- /dev/null +++ b/shaders/text_atlas_vertex.glsl @@ -0,0 +1,56 @@ +#version 440 core + +struct Vertex { + vec2 position; + vec2 tex_coord; +}; + +struct VertexOutput { + vec4 position; + vec2 tex_coord; +}; + +struct Glyph { + vec2 atlas_position; + vec2 size; + vec2 target_position; + float y_offset; + float _haha_alignment; +}; + +layout(std430, binding = 0) readonly buffer VertexBlock { + Vertex vertices[]; +}; + +layout(std430, binding = 1) readonly buffer GlyphBlock { + Glyph glyphs[]; +}; + +layout(std430, binding = 2) readonly buffer ParamsBlock { + vec2 screen_size; + vec2 font_size; +}; + +vec4 to_device_position(vec2 position, vec2 size) { + return vec4(((position / size) * 2.0) - vec2(1.0), 1.0, 1.0); +} + +layout(location = 0) out VertexOutput out_vertex; + +void main() { + Glyph glyph = glyphs[gl_InstanceID]; + + vec2 scaled_size = ((vertices[gl_VertexID].position + 1.0) / 2.0) * (glyph.size/2.0); + vec2 scaled_size_2 = ((vertices[gl_VertexID].position + 1.0) / 2.0) * (glyph.size); + vec2 glyph_pos = scaled_size + glyph.target_position + vec2(0, glyph.y_offset/2.0+font_size.y); + + vec4 device_position = to_device_position(glyph_pos, screen_size); + vec2 atlas_position = (scaled_size_2 + glyph.atlas_position) / 512.0; + + device_position.y = -device_position.y; + + out_vertex.position = device_position; + out_vertex.tex_coord = atlas_position; + + gl_Position = device_position; +} diff --git a/shaders/ui_rect_fragment.glsl b/shaders/ui_rect_fragment.glsl new file mode 100644 index 0000000..50a2eb9 --- /dev/null +++ b/shaders/ui_rect_fragment.glsl @@ -0,0 +1,45 @@ +#version 440 core + +struct UiRectFragment { + highp vec4 device_position; + highp vec2 position; + highp vec2 size; + highp vec2 border_size; + highp vec2 screen_size; + highp vec2 tex_coord; + highp vec4 color; +}; + +in UiRectFragment out_rect; + +layout(location = 0) out highp vec4 color; + +highp float rect_sdf( + highp vec2 absolute_pixel_position, + highp vec2 origin, + highp vec2 size, + highp float corner_radius +) { + highp vec2 half_size = size / 2.0; + highp vec2 rect_center = origin + half_size; + + highp vec2 pixel_position = abs(absolute_pixel_position - rect_center); + highp vec2 shrunk_corner_position = half_size - corner_radius; + + highp vec2 pixel_to_shrunk_corner = max(vec2(0), pixel_position - shrunk_corner_position); + highp float distance_to_shrunk_corner = length(pixel_to_shrunk_corner); + highp float distance = distance_to_shrunk_corner - corner_radius; + + return distance; +} + +void main() { + highp vec2 pixel_pos = out_rect.tex_coord.xy * out_rect.screen_size; + + highp float distance = rect_sdf(pixel_pos, out_rect.position, out_rect.size, out_rect.border_size.x); + if (distance <= 0.0) { + color = out_rect.color; + } else { + color = vec4(0); + } +} diff --git a/shaders/ui_rect_vertex.glsl b/shaders/ui_rect_vertex.glsl new file mode 100644 index 0000000..3a599df --- /dev/null +++ b/shaders/ui_rect_vertex.glsl @@ -0,0 +1,50 @@ +#version 440 core + +struct Vertex { + vec2 position; + vec2 tex_coord; +}; + +struct UiRect { + vec4 position; + vec4 size; + vec4 border_size; + vec4 color; +}; + +struct UiRectFragment { + highp vec4 device_position; + highp vec2 position; + highp vec2 size; + highp vec2 border_size; + highp vec2 screen_size; + highp vec2 tex_coord; + highp vec4 color; +}; + +layout(std430, binding = 0) readonly buffer VertexBlock { + Vertex vertices[]; +}; + +layout(std430, binding = 1) readonly buffer RectBlock { + UiRect rects[]; +}; +layout(std430, binding = 2) readonly buffer ParamsBlock { + vec2 screen_size; +}; + +out UiRectFragment out_rect; + +void main() { + UiRect rect = rects[gl_InstanceID]; + + out_rect.device_position = vec4(vertices[gl_VertexID].position, 1, 1); + out_rect.position = rect.position.xy; + out_rect.size = rect.size.xy; + out_rect.border_size = rect.border_size.xy; + out_rect.screen_size = screen_size; + out_rect.tex_coord = vertices[gl_VertexID].tex_coord; + out_rect.color = rect.color; + + gl_Position = vec4(vertices[gl_VertexID].position, 1, 1); +} diff --git a/src/gfx/gfx.odin b/src/gfx/gfx.odin new file mode 100644 index 0000000..b88b214 --- /dev/null +++ b/src/gfx/gfx.odin @@ -0,0 +1,53 @@ +package gfx; + +import "core:strings" +import c "core:c" + +foreign import gfx "../../bin/libgfx.a" + +GfxFrameFunc :: proc "c" (mouse_x: int, mouse_y: int, mouse_left_down: bool, mouse_right_down: bool) + +cx :: struct {} + +GpuGlyph :: struct { + atlas_position: [2]f32, + size: [2]f32, + position: [2]f32, + y_offset: f32, + _haha_alignment: f32, + color: [2]f32, +} + +@(private) +gfx_string :: struct { + data: [^]u8, + len: c.size_t, + + // unused + owned: c.bool, +} + +foreign gfx { + keep_running: bool + + @(link_name="gfx_init_context") init_context :: proc "c" (frame_func: GfxFrameFunc, width: u32, height: u32) -> ^cx --- + @(link_name="gfx_run_events") run_events :: proc "c" (gfx_cx: ^cx) --- + @(link_name="gfx_add_glyph") add_glyph :: proc "c" (gfx_cx: ^cx, glyph: GpuGlyph) --- + @(link_name="gfx_push_texture_buffer") push_texture_buffer :: proc "c" (gfx_cx: ^cx, width: u32, height: u32, data: [^]u8, len: u32) --- + + @(private) + gfx_queue_text :: proc "c" (gfx_cx: ^cx, text: gfx_string, position: [^]f32, max_x: f32, max_y: f32, color: [^]f32) --- +} + +queue_text :: proc(gfx_cx: ^cx, text: string, position: [2]f32, max_x: f32, max_y: f32, color: [4]f32) { + text_ := gfx_string { + data = raw_data(text), + len = len(text), + }; + + p := []f32 { position[0], position[1] } + c := []f32 { color[0], color[1], color[2], color[3] } + + gfx_queue_text(gfx_cx, text_, raw_data(p), max_x, max_y, raw_data(c)) +} + diff --git a/src/main.odin b/src/main.odin new file mode 100644 index 0000000..0696327 --- /dev/null +++ b/src/main.odin @@ -0,0 +1,92 @@ +package main + +import "core:runtime"; +import "core:fmt"; +import "core:os"; +import stbtt "vendor:stb/truetype"; + +import "gfx"; + +gfx_cx: ^gfx.cx = nil + +frame_func :: proc "c" (mouse_x: int, mouse_y: int, mouse_left_down: bool, mouse_right_down: bool) { + context = runtime.default_context(); + + for y in 0..<100 { + for x in 0..<100 { + gfx.queue_text(gfx_cx, "Helope!", { f32(x)*100, f32(x)*24 + f32(y)*26}, 1000, 1000, { 1, 1, 1, 1 }); + } + } +} + +main :: proc() { + gfx_cx = gfx.init_context(frame_func, 800, 600) + + font_data, success := os.read_entire_file_from_filename("./bin/JetBrainsMono-Medium.ttf"); + if !success { + fmt.eprintln("failed to read font file"); + os.exit(1); + } + + font: stbtt.fontinfo; + if stbtt.InitFont(&font, raw_data(font_data), 0) == false { + fmt.eprintln("failed to init font"); + os.exit(1); + } + + bitmap_size: i32 = 512; + bitmap := make([]u8, bitmap_size * bitmap_size); + + rasterized_font_height: i32 = 24; + + ascent, descent, line_gap: i32; + scale := stbtt.ScaleForPixelHeight(&font, f32(rasterized_font_height * 2)); + stbtt.GetFontVMetrics(&font, &ascent, &descent, &line_gap); + + gfx.add_glyph(gfx_cx, gfx.GpuGlyph { + size = {f32(rasterized_font_height/4), 1}, + y_offset = f32(-rasterized_font_height), + }); + + x := rasterized_font_height / 4; + y: i32 = 0; + for i in 33..<33+96 { + width, height, xoff, yoff: i32; + glyph_bitmap := stbtt.GetCodepointBitmap( + &font, scale, scale, rune(i), &width, &height, &xoff, &yoff); + + if (x + width) >= bitmap_size { + x = 0; + y += i32(f32(ascent - descent + line_gap) * scale); + } + + xxx: i32 = x; + for xx in 0.. +#include +#include +#include + +#define array(T) struct T##_Array +#define arrayTemplate(T) \ + array(T) { \ + size_t size, capacity; \ + T *data; \ + }; \ + array(T) T##_ArrayConstruct(size_t size) { \ + array(T) this; \ + this.size = 0; \ + this.capacity = size; \ + this.data = malloc(size * sizeof(T)); \ + if (!this.data) { \ + assert("failed to allocate memory for array"); \ + } \ + return this; \ + }; \ + void T##_PushArray(array(T) * arr, T value) { \ + if (arr->size == arr->capacity) { \ + arr->capacity *= 2; \ + void *new_data = realloc(arr->data, arr->capacity * sizeof(T)); \ + if (new_data == NULL) { \ + fprintf(stderr, "out of memory when reallocating array\n"); \ + } \ + arr->data = new_data; \ + } \ + if (arr->size + 1 <= arr->capacity) { \ + arr->data[arr->size] = value; \ + arr->size += 1; \ + } \ + }; \ + void T##_PushArrayMulti(array(T) * arr, T * values, size_t len) { \ + for (size_t i = 0; i < len; ++i) { \ + T##_PushArray(arr, values[i]); \ + } \ + }; \ + void T##_InsertArrayAt(array(T) * arr, size_t loc, T value) { \ + if (arr->size == arr->capacity) { \ + arr->capacity *= 2; \ + void *new_data = realloc(arr->data, arr->capacity * sizeof(T)); \ + if (new_data == NULL) { \ + fprintf(stderr, "out of memory when reallocating array\n"); \ + } \ + arr->data = new_data; \ + } \ + memcpy(&arr->data[loc + 1], &arr->data[loc], arr->size * sizeof(T)); \ + arr->data[loc] = value; \ + arr->size += 1; \ + }; + +#define slice(T) struct T##_Slice +#define sliceTemplate(T) \ + slice(T) { \ + size_t len; \ + T *data; \ + }; \ + array(T) T##_FromSlice(slice(T) s) { \ + array(T) arr = T##_ArrayConstruct(s.len); \ + memcpy(arr.data, s.data, sizeof(T) * s.len); \ + arr.size = s.len; \ + return arr; \ + } \ + slice(T) T##_SliceConstruct(void *data, size_t len) { \ + slice(T) this; \ + this.len = len; \ + this.data = data; \ + return this; \ + } + +#define newArray(T, size) T##_ArrayConstruct(size) +#define newArrayFromSlice(T, s) T##_FromSlice(s) +#define pushArray(T, arr, value) T##_PushArray(arr, (value)) +#define pushArrayMulti(T, arr, values, len) T##_PushArrayMulti(arr, values, len) + +#define insertArrayAt(T, arr, loc, value) T##_InsertArrayAt(arr, loc, (value)) + +#define newSlice(T, data, len) T##_SliceConstruct(data, len) + +arrayTemplate(uint8_t); +sliceTemplate(uint8_t); +arrayTemplate(uint32_t); +sliceTemplate(uint32_t); + +#endif diff --git a/vendor/pcleavelin/gfx.h b/vendor/pcleavelin/gfx.h new file mode 100644 index 0000000..49d9e5b --- /dev/null +++ b/vendor/pcleavelin/gfx.h @@ -0,0 +1,1347 @@ +// Graphics layer abstraction. + +#ifndef ED_GFX_INCLUDED +#define ED_GFX_INCLUDED +#include + +#include "ed_array.h" +#include "string.h" + +bool keep_running = true; + +#if defined(__APPLE__) +#include +#include +#include +#include +#include +#include +@interface EDGFXView : NSView +@property NSTrackingArea *tracking_area; +@end +@interface AppDelegate : NSObject +@end +@interface WindowDelegate : NSObject +@end + +#define wrapIdArray(T) \ + typedef id _##T; \ + arrayTemplate(_##T); + +wrapIdArray(MTLRenderPipelineState); +wrapIdArray(MTLBuffer); +wrapIdArray(MTLTexture); +#endif + +typedef struct { + float position[4]; + float size[4]; + float border_size[4]; + float color[4]; +} GpuUiRect; +arrayTemplate(GpuUiRect); + +typedef struct { + float atlas_position[2]; + float size[2]; + float position[2]; + float y_offset; + float _haha_alignment; + float color[4]; +} GpuGlyph; +arrayTemplate(GpuGlyph); + +typedef struct { + float screen_size[2]; + float font_size[2]; +} GpuUniformParams; + +#if defined(__APPLE__) +typedef struct { + NSApplication *application; + NSWindow *window; + EDGFXView *view; + bool keep_running; + bool refresh_now; + + int mouse_x, mouse_y; + bool mouse_left_down, mouse_right_down; + + // Metal objects + id device; + CAMetalLayer *metal_layer; + id library; + id command_queue; + + array(_MTLRenderPipelineState) pipelines; + array(_MTLBuffer) buffers; + array(_MTLTexture) textures; +} _metal_gfx_context; +#elif __linux__ +#include "wayland-crap/xdg-shell.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#include "file_io.h" + +// And I thought MacOS needed a lot of state to create a window +arrayTemplate(GLuint); +typedef struct { + struct wl_display *display; + struct wl_registry *registry; + struct wl_surface *surface; + struct wl_compositor *compositor; + struct wl_shm *shared_memory; + struct wl_shm_pool *shared_memory_pool; + struct xdg_wm_base *wm_base; + struct wl_buffer *buffer; + struct xdg_surface *xdg_surface; + struct xdg_toplevel *xdg_toplevel; + struct wl_seat *seat; + struct wl_pointer *pointer; + uint8_t *pixels; + + EGLDisplay egl_display; + EGLConfig egl_config; + EGLSurface egl_surface; + EGLContext egl_context; + struct wl_egl_window *egl_window; + + int mouse_x, mouse_y; + int mouse_left_down, mouse_right_down; + + GLuint ui_rect_vertex_shader; + GLuint ui_rect_fragment_shader; + GLuint text_atlas_vertex_shader; + GLuint text_atlas_fragment_shader; + GLuint ui_rect_shader_program; + GLuint text_atlas_shader_program; + + array(GLuint) buffers; + array(GLuint) textures; +} _opengl_gfx_context_wayland; + +static void _opengl_gfx_present_wayland(_opengl_gfx_context_wayland *cx); + +typedef struct { + Display *display; + Window window; + int screen; +} _opengl_gfx_context_x11; +#endif + +typedef void (*_gfx_frame_func)(int mouse_x, int mouse_y, bool mouse_left_down, + bool mouse_right_down); +typedef struct { +#if defined(__APPLE__) + _metal_gfx_context backend; +#elif __linux__ + // TODO: be able to use X11 or Wayland at runtime + _opengl_gfx_context_wayland backend; +#else +#error "Unsupported platform" +#endif + + uint32_t frame_width; + uint32_t frame_height; + _gfx_frame_func frame_func; + + array(GpuUiRect) gpu_ui_rects; + array(GpuGlyph) gpu_glyphs; + array(GpuGlyph) glyph_cache; +} gfx_context_t; +static gfx_context_t _gfx_context; + +#ifdef ED_GFX_IMPLEMENTATION + +#if defined(__APPLE__) +static void _metal_gfx_present(_metal_gfx_context *cx); +static void _metal_gfx_send_events(_metal_gfx_context *cx); + +void gfx_update_buffer(gfx_context_t *cx, size_t buffer_index, const void *data, + size_t len); + +@implementation AppDelegate +- (NSApplicationTerminateReply)applicationShouldTerminate: + (NSApplication *)sender { + keep_running = false; + + return NSTerminateCancel; +} + +- (BOOL)applicationShouldTerminateAfterLastWindowClosed: + (NSApplication *)sender { + keep_running = false; + + return NO; +} +@end + +@implementation WindowDelegate +- (BOOL)windowShouldClose:(NSApplication *)sender { + keep_running = false; + return YES; +} + +- (void)windowDidResize:(NSNotification *)notification { + _gfx_context.frame_width = + _gfx_context.backend.window.contentView.frame.size.width; + _gfx_context.frame_height = + _gfx_context.backend.window.contentView.frame.size.height; + + CGFloat scale = _gfx_context.backend.metal_layer.contentsScale; + [_gfx_context.backend.metal_layer + setDrawableSize:CGSizeMake(_gfx_context.frame_width * scale, + _gfx_context.frame_height * scale)]; + + _gfx_context.backend.refresh_now = true; + _gfx_context.backend.view.needsDisplay = true; +} +@end + +@implementation EDGFXView +- (BOOL)isOpaque { + return YES; +} +- (void)updateLayer { + _metal_gfx_present(&_gfx_context.backend); +} + +- (BOOL)wantsLayer { + return YES; +} +- (BOOL)wantsUpdateLayer { + return YES; +} +- (NSViewLayerContentsRedrawPolicy)layerContentsRedrawPolicy { + return NSViewLayerContentsRedrawOnSetNeedsDisplay; +} + +- (id)initWithFrame:(NSRect)frameRect { + self = [super initWithFrame:frameRect]; + NSRect rect = + NSMakeRect(0, 0, + _gfx_context.frame_width = + _gfx_context.backend.window.contentView.frame.size.width, + _gfx_context.frame_height = + _gfx_context.backend.window.contentView.frame.size.height); + + self.tracking_area = [[NSTrackingArea alloc] + initWithRect:rect + options:(NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | + NSTrackingActiveInKeyWindow) + owner:self + userInfo:nil]; + [self addTrackingArea:self.tracking_area]; + + return self; +} + +- (void)mouseDown:(NSEvent *)event { + if ((unsigned long)event.type == NSEventTypeLeftMouseDown) { + _gfx_context.backend.mouse_left_down = true; + } + if (event.type == NSEventTypeRightMouseUp) { + _gfx_context.backend.mouse_right_down = true; + } + + _gfx_context.gpu_glyphs.size = 0; + _gfx_context.gpu_ui_rects.size = 0; + _gfx_context.frame_func(_gfx_context.backend.mouse_x, + _gfx_context.frame_height - + _gfx_context.backend.mouse_y, + _gfx_context.backend.mouse_left_down, + _gfx_context.backend.mouse_right_down); + [self setNeedsDisplay:YES]; +} + +- (void)mouseUp:(NSEvent *)event { + if ((unsigned long)event.type == NSEventTypeLeftMouseUp) { + _gfx_context.backend.mouse_left_down = false; + } + if (event.type == NSEventTypeRightMouseDown) { + _gfx_context.backend.mouse_right_down = false; + } + + _gfx_context.gpu_glyphs.size = 0; + _gfx_context.gpu_ui_rects.size = 0; + _gfx_context.frame_func(_gfx_context.backend.mouse_x, + _gfx_context.frame_height - + _gfx_context.backend.mouse_y, + _gfx_context.backend.mouse_left_down, + _gfx_context.backend.mouse_right_down); + [self setNeedsDisplay:YES]; +} + +- (void)mouseDragged:(NSEvent *)event { + NSPoint location = [self convertPoint:[event locationInWindow] fromView:nil]; + + _gfx_context.backend.mouse_x = location.x; + _gfx_context.backend.mouse_y = location.y; + + _gfx_context.gpu_glyphs.size = 0; + _gfx_context.gpu_ui_rects.size = 0; + _gfx_context.frame_func(_gfx_context.backend.mouse_x, + _gfx_context.frame_height - + _gfx_context.backend.mouse_y, + _gfx_context.backend.mouse_left_down, + _gfx_context.backend.mouse_right_down); + + [self setNeedsDisplay:YES]; +} + +- (void)mouseMoved:(NSEvent *)event { + NSPoint location = [self convertPoint:[event locationInWindow] fromView:nil]; + + _gfx_context.backend.mouse_x = location.x; + _gfx_context.backend.mouse_y = location.y; + + [self setNeedsDisplay:YES]; + [self displayIfNeeded]; + [_gfx_context.backend.metal_layer setNeedsDisplay]; + [_gfx_context.backend.metal_layer displayIfNeeded]; +} + +- (void)updateTrackingAreas { + [self removeTrackingArea:self.tracking_area]; + [self.tracking_area release]; + + NSRect rect = + NSMakeRect(0, 0, + _gfx_context.frame_width = + _gfx_context.backend.window.contentView.frame.size.width, + _gfx_context.frame_height = + _gfx_context.backend.window.contentView.frame.size.height); + + self.tracking_area = [[NSTrackingArea alloc] + initWithRect:rect + options:(NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | + NSTrackingActiveInKeyWindow) + owner:self + userInfo:nil]; + [self addTrackingArea:self.tracking_area]; +} +@end + +static _metal_gfx_context _metal_gfx_init_context(uint32_t width, + uint32_t height) { + NSApplication *application = [NSApplication sharedApplication]; + if (application == NULL) { + fprintf(stderr, "NSApplication:sharedApplication failed\n"); + exit(1); + } + + NSString *title = @"chat - [Slack Sux]"; + + NSRect rect = NSMakeRect(0, 0, width, height); + NSWindow *window = [[NSWindow alloc] + initWithContentRect:rect + styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | + NSWindowStyleMaskMiniaturizable | + NSWindowStyleMaskResizable + backing:NSBackingStoreBuffered + defer:NO]; + EDGFXView *view = [[EDGFXView alloc] initWithFrame:rect]; + [view updateTrackingAreas]; + [window setTitle:title]; + [window setContentView:view]; + [window setDelegate:[[WindowDelegate alloc] init]]; + [window makeKeyAndOrderFront:NULL]; + + // TODO: make this work + // if (application.mainMenu == NULL) { + // NSMenu *menu = [[NSMenu alloc] initWithTitle:@"an_editor"]; + // if (menu == NULL) { + // fprintf(stderr, "failed to create application menu\n"); + // exit(1); + // } + // application.mainMenu = menu; + // } + [application setDelegate:[[AppDelegate alloc] init]]; + [application setActivationPolicy:NSApplicationActivationPolicyRegular]; + [application setPresentationOptions:NSApplicationPresentationDefault]; + [application finishLaunching]; + + id device = MTLCreateSystemDefaultDevice(); + + CAMetalLayer *metal_layer = [CAMetalLayer layer]; + metal_layer.device = device; + metal_layer.pixelFormat = MTLPixelFormatRGBA8Unorm; + metal_layer.frame = CGRectMake(0, 0, width, height); + metal_layer.needsDisplayOnBoundsChange = YES; + metal_layer.presentsWithTransaction = YES; + metal_layer.autoresizingMask = kCALayerWidthSizable | kCALayerHeightSizable; + + // TODO: set this to the display dpi scale + metal_layer.contentsScale = 2.0; + view.wantsLayer = YES; + [view.layer addSublayer:metal_layer]; + view.layerContentsRedrawPolicy = NSViewLayerContentsRedrawOnSetNeedsDisplay; + + NSError *libraryError = NULL; + NSURL *libraryURL = [[NSBundle mainBundle] URLForResource:@"./shaders" + withExtension:@"metallib"]; + if (libraryURL == NULL) { + fprintf(stderr, "Couldn't find library file\n"); + exit(1); + } + + id library = + [device newLibraryWithURL:libraryURL error:&libraryError]; + + if (library == NULL) { + if (libraryError.description != NULL) { + NSLog(@"Error description: %@\n", libraryError.description); + } + + exit(1); + } + + id command_queue = [device newCommandQueue]; + id vertex_func = [library newFunctionWithName:@"vs_main"]; + id fragment_func = [library newFunctionWithName:@"fs_main"]; + id ui_rect_vertex_func = + [library newFunctionWithName:@"ui_rect_vs"]; + id ui_rect_fragment_func = + [library newFunctionWithName:@"ui_rect_fs"]; + + MTLRenderPipelineDescriptor *pipeline_descriptor = + [[MTLRenderPipelineDescriptor alloc] init]; + [pipeline_descriptor setVertexFunction:vertex_func]; + [pipeline_descriptor setFragmentFunction:fragment_func]; + pipeline_descriptor.colorAttachments[0].pixelFormat = + MTLPixelFormatRGBA8Unorm; + pipeline_descriptor.colorAttachments[0].alphaBlendOperation = + MTLBlendOperationAdd; + pipeline_descriptor.colorAttachments[0].sourceAlphaBlendFactor = + MTLBlendFactorSourceAlpha; + pipeline_descriptor.colorAttachments[0].destinationAlphaBlendFactor = + MTLBlendFactorOneMinusSourceAlpha; + pipeline_descriptor.colorAttachments[0].sourceRGBBlendFactor = + MTLBlendFactorSourceAlpha; + pipeline_descriptor.colorAttachments[0].destinationRGBBlendFactor = + MTLBlendFactorOneMinusSourceAlpha; + pipeline_descriptor.colorAttachments[0].blendingEnabled = true; + + MTLRenderPipelineDescriptor *ui_rect_pipeline_descriptor = + [[MTLRenderPipelineDescriptor alloc] init]; + [ui_rect_pipeline_descriptor setVertexFunction:ui_rect_vertex_func]; + [ui_rect_pipeline_descriptor setFragmentFunction:ui_rect_fragment_func]; + ui_rect_pipeline_descriptor.colorAttachments[0].pixelFormat = + MTLPixelFormatRGBA8Unorm; + ui_rect_pipeline_descriptor.colorAttachments[0].alphaBlendOperation = + MTLBlendOperationAdd; + ui_rect_pipeline_descriptor.colorAttachments[0].sourceAlphaBlendFactor = + MTLBlendFactorSourceAlpha; + ui_rect_pipeline_descriptor.colorAttachments[0].destinationAlphaBlendFactor = + MTLBlendFactorOneMinusSourceAlpha; + ui_rect_pipeline_descriptor.colorAttachments[0].sourceRGBBlendFactor = + MTLBlendFactorSourceAlpha; + ui_rect_pipeline_descriptor.colorAttachments[0].destinationRGBBlendFactor = + MTLBlendFactorOneMinusSourceAlpha; + ui_rect_pipeline_descriptor.colorAttachments[0].blendingEnabled = true; + + NSError *pipeline_error = NULL; + array(_MTLRenderPipelineState) pipelines = + newArray(_MTLRenderPipelineState, 2); + pushArray(_MTLRenderPipelineState, &pipelines, + [device newRenderPipelineStateWithDescriptor:pipeline_descriptor + error:&pipeline_error]); + if (pipeline_error != NULL) { + if (pipeline_error.description != NULL) { + NSLog(@"Error description: %@\n", pipeline_error.description); + } + + exit(1); + } + pushArray(_MTLRenderPipelineState, &pipelines, + [device + newRenderPipelineStateWithDescriptor:ui_rect_pipeline_descriptor + error:&pipeline_error]); + + array(_MTLBuffer) buffers = newArray(_MTLBuffer, 8); + array(_MTLTexture) textures = newArray(_MTLTexture, 8); + + if (pipeline_error != NULL) { + if (pipeline_error.description != NULL) { + NSLog(@"Error description: %@\n", pipeline_error.description); + } + + exit(1); + } + + return (_metal_gfx_context){ + .application = application, + .window = window, + .view = view, + .keep_running = true, + + .device = device, + .metal_layer = metal_layer, + .library = library, + .command_queue = command_queue, + .pipelines = pipelines, + .buffers = buffers, + .textures = textures, + }; +} + +static void _metal_gfx_send_events(_metal_gfx_context *cx) { + NSEvent *event = [cx->application nextEventMatchingMask:NSEventMaskAny + untilDate:[NSDate distantPast] + inMode:NSDefaultRunLoopMode + dequeue:YES]; + + [cx->application sendEvent:event]; +} + +static void _metal_gfx_present(_metal_gfx_context *cx) { + _gfx_context.gpu_glyphs.size = 0; + _gfx_context.gpu_ui_rects.size = 0; + _gfx_context.frame_func(cx->mouse_x, _gfx_context.frame_height - cx->mouse_y, + cx->mouse_left_down, cx->mouse_right_down); + + if (_gfx_context.gpu_glyphs.size > 0) { + gfx_update_buffer(&_gfx_context, 2, _gfx_context.gpu_glyphs.data, + _gfx_context.gpu_glyphs.size * sizeof(GpuGlyph)); + } + if (_gfx_context.gpu_ui_rects.size > 0) { + gfx_update_buffer(&_gfx_context, 4, _gfx_context.gpu_ui_rects.data, + _gfx_context.gpu_ui_rects.size * sizeof(GpuUiRect)); + } + + GpuUniformParams gpu_uniform_params = { + .screen_size = + { + (float)_gfx_context.frame_width, + (float)_gfx_context.frame_height, + }, + .font_size = + { + (float)_FONT_WIDTH, + (float)_FONT_HEIGHT, + }, + }; + + gfx_update_buffer(&_gfx_context, 3, &gpu_uniform_params, + sizeof(GpuUniformParams)); + + @autoreleasepool { + id drawable = [cx->metal_layer nextDrawable]; + + id command_buffer = [cx->command_queue commandBuffer]; + MTLRenderPassDescriptor *render_pass_desc = + [MTLRenderPassDescriptor renderPassDescriptor]; + render_pass_desc.colorAttachments[0].texture = drawable.texture; + render_pass_desc.colorAttachments[0].loadAction = MTLLoadActionClear; + render_pass_desc.colorAttachments[0].clearColor = + MTLClearColorMake(0.1, 0.1, 0.1, 1); + render_pass_desc.colorAttachments[0].storeAction = MTLStoreActionStore; + + id encoder = + [command_buffer renderCommandEncoderWithDescriptor:render_pass_desc]; + + if (_gfx_context.gpu_ui_rects.size > 0) { + // UI Rects + [encoder setRenderPipelineState:cx->pipelines.data[1]]; + // FIXME: allow these to be described by the user instead of + // hardcoded + [encoder setVertexBuffer:cx->buffers.data[0] + offset:0 + atIndex:0]; // vertices + [encoder setVertexBuffer:cx->buffers.data[4] + offset:0 + atIndex:1]; // ui rects + [encoder setVertexBuffer:cx->buffers.data[3] + offset:0 + atIndex:2]; // uniforms + [encoder drawIndexedPrimitives:MTLPrimitiveTypeTriangle + indexCount:6 + indexType:MTLIndexTypeUInt16 + indexBuffer:cx->buffers.data[1] + indexBufferOffset:0 + instanceCount:_gfx_context.gpu_ui_rects.size]; + } + + if (_gfx_context.gpu_glyphs.size > 0) { + // UI Text + [encoder setRenderPipelineState:cx->pipelines.data[0]]; + // FIXME: allow these to be described by the user instead of + // hardcoded + [encoder setVertexBuffer:cx->buffers.data[0] + offset:0 + atIndex:0]; // vertices + [encoder setVertexBuffer:cx->buffers.data[2] + offset:0 + atIndex:1]; // glyph data + [encoder setVertexBuffer:cx->buffers.data[3] + offset:0 + atIndex:2]; // uniforms + [encoder setFragmentTexture:cx->textures.data[0] atIndex:0]; + [encoder drawIndexedPrimitives:MTLPrimitiveTypeTriangle + indexCount:6 + indexType:MTLIndexTypeUInt16 + indexBuffer:cx->buffers.data[1] + indexBufferOffset:0 + instanceCount:_gfx_context.gpu_glyphs.size]; + } + + [encoder endEncoding]; + // FIXME: `afterMinimumDuration` causes the weird re-size scaling, but I + // need to figure why the heck the NSView doesn't get the rendered + // contents unless `afterMinimumDuration` is here + if (cx->refresh_now) { + [command_buffer presentDrawable:drawable]; + cx->refresh_now = false; + } else { + [command_buffer presentDrawable:drawable + afterMinimumDuration:1.0 / 144.0]; + } + [command_buffer commit]; + + [command_buffer waitUntilScheduled]; + } +} + +static size_t _metal_gfx_push_texture_buffer(_metal_gfx_context *cx, + uint32_t width, uint32_t height, + const void *data, size_t len) { + MTLTextureDescriptor *texture_desc = [MTLTextureDescriptor + texture2DDescriptorWithPixelFormat:MTLPixelFormatR8Unorm + width:width + height:height + mipmapped:false]; + _MTLTexture texture = [cx->device newTextureWithDescriptor:texture_desc]; + + MTLRegion region = MTLRegionMake2D(0, 0, width, height); + [texture replaceRegion:region + mipmapLevel:0 + slice:0 + withBytes:data + bytesPerRow:width * sizeof(uint8_t) + bytesPerImage:len]; + + pushArray(_MTLTexture, &cx->textures, texture); + return cx->textures.size - 1; +} + +static void _metal_gfx_resize_texture_buffer(_metal_gfx_context *cx, + uint32_t width, + size_t texture_index, + uint32_t height) { + [cx->textures.data[texture_index] setPurgeableState:MTLPurgeableStateEmpty]; + [cx->textures.data[texture_index] release]; + + MTLTextureDescriptor *texture_desc = [MTLTextureDescriptor + texture2DDescriptorWithPixelFormat:MTLPixelFormatR8Unorm + width:width + height:height + mipmapped:false]; + cx->textures.data[texture_index] = + [cx->device newTextureWithDescriptor:texture_desc]; +} + +size_t _metal_gfx_push_vertex_buffer(_metal_gfx_context *cx, const void *data, + size_t len) { + pushArray(_MTLBuffer, &cx->buffers, + [cx->device newBufferWithBytes:data + length:len + options:MTLResourceStorageModeShared]); + + return cx->buffers.size - 1; +} +static size_t _metal_gfx_allocate_vertex_buffer(_metal_gfx_context *cx, + size_t len) { + pushArray(_MTLBuffer, &cx->buffers, + [cx->device newBufferWithLength:len + options:MTLResourceStorageModeShared]); + + return cx->buffers.size - 1; +} +static void _metal_gfx_update_buffer(_metal_gfx_context *cx, + size_t buffer_index, const void *data, + size_t len) { + void *buffer_contents = [cx->buffers.data[buffer_index] contents]; + + // FIXME: actually check to see if this will fit in the buffer + memcpy(buffer_contents, data, len); +} +#elif __linux__ +#include + +static void _wayland_pointer_enter(void *data, struct wl_pointer *pointer, + uint32_t serial, struct wl_surface *surface, + wl_fixed_t x, wl_fixed_t y) { + // fprintf(stderr, "pointer enter: %d, %d\n", x, y); +} +static void _wayland_pointer_leave(void *data, struct wl_pointer *pointer, + uint32_t serial, + struct wl_surface *surface) { + // fprintf(stderr, "pointer leave\n"); +} +static void _wayland_pointer_button(void *data, struct wl_pointer *pointer, + uint32_t serial, uint32_t time, + uint32_t button, uint32_t state) { + _opengl_gfx_context_wayland *cx = data; + + if (state == WL_POINTER_BUTTON_STATE_PRESSED) { + if (button == BTN_LEFT) { + cx->mouse_left_down = true; + } else if (button == BTN_RIGHT) { + cx->mouse_right_down = true; + } + } else if (state == WL_POINTER_BUTTON_STATE_RELEASED) { + if (button == BTN_LEFT) { + cx->mouse_left_down = false; + } else if (button == BTN_RIGHT) { + cx->mouse_right_down = false; + } + } +} +static void _wayland_pointer_axis(void *data, struct wl_pointer *pointer, + uint32_t time, uint32_t axis, + wl_fixed_t value) {} +static void _wayland_pointer_motion(void *data, struct wl_pointer *pointer, + uint32_t time, wl_fixed_t x, wl_fixed_t y) { + _opengl_gfx_context_wayland *cx = data; + cx->mouse_x = wl_fixed_to_int(x); + cx->mouse_y = wl_fixed_to_int(y); +} + +static const struct wl_pointer_listener pointer_listener = { + .enter = _wayland_pointer_enter, + .leave = _wayland_pointer_leave, + .motion = _wayland_pointer_motion, + .button = _wayland_pointer_button, + .axis = _wayland_pointer_axis, +}; + +static void _wayland_xdg_toplevel_configure(void *data, + struct xdg_toplevel *xdg_toplevel, + int32_t width, int32_t height, + struct wl_array *states) {} + +static void _wayland_xdg_toplevel_close(void *data, + struct xdg_toplevel *xdg_toplevel) { + keep_running = false; +} + +static const struct xdg_toplevel_listener xdg_toplevel_listener = { + _wayland_xdg_toplevel_configure, + _wayland_xdg_toplevel_close, +}; + +static void _wayland_xdg_surface_configure(void *data, + struct xdg_surface *xdg_surface, + uint32_t serial) { + xdg_surface_ack_configure(xdg_surface, serial); +} + +static const struct xdg_surface_listener xdg_surface_listener = { + _wayland_xdg_surface_configure, +}; + +static void _wayland_xdg_wm_base_ping(void *data, struct xdg_wm_base *shell, + uint32_t serial) { + xdg_wm_base_pong(shell, serial); +} +static const struct xdg_wm_base_listener xdg_wm_base_listener = { + _wayland_xdg_wm_base_ping, +}; + +static void _wayland_registry_handle_global(void *data, + struct wl_registry *registry, + uint32_t name, + const char *interface, + uint32_t version) { + _opengl_gfx_context_wayland *d = data; + + fprintf(stderr, "global: %s\n", interface); + + if (strcmp(interface, "wl_compositor") == 0) { + d->compositor = + wl_registry_bind(registry, name, &wl_compositor_interface, 3); + } else if (strcmp(interface, "wl_shm") == 0) { + d->shared_memory = wl_registry_bind(registry, name, &wl_shm_interface, 1); + } else if (strcmp(interface, "xdg_wm_base") == 0) { + d->wm_base = wl_registry_bind(registry, name, &xdg_wm_base_interface, 1); + } else if (strcmp(interface, "wl_seat") == 0) { + d->seat = wl_registry_bind(registry, name, &wl_seat_interface, 1); + } +} + +static void _wayland_registry_handle_global_remove(void *data, + struct wl_registry *registry, + uint32_t name) {} + +static const struct wl_registry_listener registry_listener = { + _wayland_registry_handle_global, + _wayland_registry_handle_global_remove, +}; + +static void _opengl_gfx_message_callback(GLenum source, GLenum type, GLenum id, + GLenum severity, GLsizei length, + const GLchar *message, + const void *user_param) { + fprintf(stderr, + "GL CALLBACK: %s type = 0x%x, severity = 0x%x, message = %s\n", + type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : "", type, severity, + message); +} + +static void _opengl_gfx_check_shader_error(string msg, GLuint shader, + GLuint status) { + GLint good = 0; + glGetShaderiv(shader, status, &good); + if (good == GL_FALSE) { + GLint max_length = 0; + glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &max_length); + + uint8_t *log_buffer = malloc(max_length + 1); + glGetShaderInfoLog(shader, max_length, &max_length, log_buffer); + glDeleteShader(shader); + + fprintf(stderr, "%.*s: %.*s\n", msg.len, msg.data, max_length, log_buffer); + exit(1); + } +} + +static void _opengl_gfx_check_shader_program_error(string msg, + GLuint shader_program, + GLuint status) { + GLint good = 0; + glGetProgramiv(shader_program, status, &good); + if (good == GL_FALSE) { + GLint max_length = 0; + glGetProgramiv(shader_program, GL_INFO_LOG_LENGTH, &max_length); + + uint8_t *log_buffer = malloc(max_length + 1); + glGetProgramInfoLog(shader_program, max_length, &max_length, log_buffer); + glDeleteProgram(shader_program); + + fprintf(stderr, "%.*s: %.*s\n", msg.len, msg.data, max_length, log_buffer); + exit(1); + } +} + +static GLuint _opengl_gfx_compile_shader(string file_path, GLuint shader_type) { + GLuint shader = glCreateShader(shader_type); + size_t shader_file_size = get_file_size(file_path); + uint8_t *shader_file_data = malloc(shader_file_size + 1); + load_file(file_path, shader_file_size, shader_file_data); + shader_file_data[shader_file_size] = 0; + // fprintf(stderr, "%s\n", shader_file_data); + + glShaderSource(shader, 1, &shader_file_data, NULL); + glCompileShader(shader); + + _opengl_gfx_check_shader_error(_String("failed to compile shader"), shader, + GL_COMPILE_STATUS); + + return shader; +} + +static _opengl_gfx_context_wayland +_opengl_gfx_init_context_wayland(uint32_t width, uint32_t height) { + _opengl_gfx_context_wayland cx = {0}; + + cx.display = wl_display_connect(NULL); + if (!cx.display) { + fprintf(stderr, "Failed to connect to Wayland display\n"); + exit(1); + } + struct wl_registry *registry = wl_display_get_registry(cx.display); + wl_registry_add_listener(registry, ®istry_listener, &cx); + + // wait for all the globals to be registered + wl_display_roundtrip(cx.display); + xdg_wm_base_add_listener(cx.wm_base, &xdg_wm_base_listener, + &_gfx_context.backend); + + cx.pointer = wl_seat_get_pointer(cx.seat); + wl_pointer_add_listener(cx.pointer, &pointer_listener, &_gfx_context.backend); + + cx.surface = wl_compositor_create_surface(cx.compositor); + cx.xdg_surface = xdg_wm_base_get_xdg_surface(cx.wm_base, cx.surface); + xdg_surface_add_listener(cx.xdg_surface, &xdg_surface_listener, + &_gfx_context.backend); + + cx.xdg_toplevel = xdg_surface_get_toplevel(cx.xdg_surface); + xdg_toplevel_add_listener(cx.xdg_toplevel, &xdg_toplevel_listener, + &_gfx_context.backend); + xdg_toplevel_set_title(cx.xdg_toplevel, "chat - [Slack sux]"); + xdg_toplevel_set_app_id(cx.xdg_toplevel, "nl.spacegirl.a_chat_client"); + + wl_surface_commit(cx.surface); + wl_display_roundtrip(cx.display); + + int buffer_size = width * height * 4; + int fd = syscall(SYS_memfd_create, "buffer", 0); + ftruncate(fd, buffer_size); + + cx.pixels = + mmap(NULL, buffer_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + for (int i = 0; i < buffer_size; i++) { + cx.pixels[i] = 0; + } + + cx.shared_memory_pool = wl_shm_create_pool(cx.shared_memory, fd, buffer_size); + cx.buffer = wl_shm_pool_create_buffer(cx.shared_memory_pool, 0, width, height, + width * 4, WL_SHM_FORMAT_ARGB8888); + + wl_surface_attach(cx.surface, cx.buffer, 0, 0); + wl_surface_commit(cx.surface); + + /* Init EGL */ + EGLint major, minor, count, n, size; + EGLConfig *configs; + EGLint config_attribs[] = { + EGL_SURFACE_TYPE, + EGL_WINDOW_BIT, + // + EGL_RED_SIZE, + 8, + // + EGL_BLUE_SIZE, + 8, + // + EGL_GREEN_SIZE, + 8, + // + EGL_RENDERABLE_TYPE, + EGL_OPENGL_BIT, + // + EGL_NONE, + }; + + static const EGLint context_attribs[] = {EGL_CONTEXT_CLIENT_VERSION, 2, + EGL_NONE}; + + cx.egl_display = eglGetDisplay(cx.display); + if (cx.egl_display == EGL_NO_DISPLAY) { + fprintf(stderr, "Failed to create EGL display\n"); + exit(1); + } + fprintf(stderr, "Created EGL display\n"); + + if (eglInitialize(cx.egl_display, &major, &minor) != EGL_TRUE) { + fprintf(stderr, "Failed to initialize EGL display\n"); + exit(1); + } + fprintf(stderr, "EGL major: %d, minor: %d\n", major, minor); + + eglGetConfigs(cx.egl_display, NULL, 0, &count); + configs = calloc(count, sizeof(EGLConfig)); + + eglChooseConfig(cx.egl_display, config_attribs, configs, count, &n); + + for (int i = 0; i < n; ++i) { + eglGetConfigAttrib(cx.egl_display, configs[i], EGL_BUFFER_SIZE, &size); + fprintf(stderr, "EGL Buffer size: %d\n", size); + + eglGetConfigAttrib(cx.egl_display, configs[i], EGL_RED_SIZE, &size); + fprintf(stderr, "EGL Red size: %d\n", size); + + cx.egl_config = configs[i]; + break; + } + + eglBindAPI(EGL_OPENGL_API); + cx.egl_context = eglCreateContext(cx.egl_display, cx.egl_config, + EGL_NO_CONTEXT, context_attribs); + + cx.egl_window = wl_egl_window_create(cx.surface, width, height); + if (cx.egl_window == EGL_NO_SURFACE) { + fprintf(stderr, "Failed to create EGL window\n"); + exit(1); + } + fprintf(stderr, "Created EGL window\n"); + + cx.egl_surface = eglCreateWindowSurface(cx.egl_display, cx.egl_config, + cx.egl_window, NULL); + if (eglMakeCurrent(cx.egl_display, cx.egl_surface, cx.egl_surface, + cx.egl_context) != EGL_TRUE) { + fprintf(stderr, "eglMakeCurrent() failed\n"); + } + + glEnable(GL_DEBUG_OUTPUT); + glDebugMessageCallback(_opengl_gfx_message_callback, NULL); + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glBlendFunc(GL_DST_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + cx.ui_rect_vertex_shader = _opengl_gfx_compile_shader( + _String("shaders/ui_rect_vertex.glsl"), GL_VERTEX_SHADER); + cx.ui_rect_fragment_shader = _opengl_gfx_compile_shader( + _String("shaders/ui_rect_fragment.glsl"), GL_FRAGMENT_SHADER); + cx.ui_rect_shader_program = glCreateProgram(); + glAttachShader(cx.ui_rect_shader_program, cx.ui_rect_vertex_shader); + glAttachShader(cx.ui_rect_shader_program, cx.ui_rect_fragment_shader); + glLinkProgram(cx.ui_rect_shader_program); + _opengl_gfx_check_shader_program_error( + _String("failed to link ui_rect shader program"), + cx.ui_rect_shader_program, GL_LINK_STATUS); + + cx.text_atlas_vertex_shader = _opengl_gfx_compile_shader( + _String("shaders/text_atlas_vertex.glsl"), GL_VERTEX_SHADER); + cx.text_atlas_fragment_shader = _opengl_gfx_compile_shader( + _String("shaders/text_atlas_fragment.glsl"), GL_FRAGMENT_SHADER); + cx.text_atlas_shader_program = glCreateProgram(); + glAttachShader(cx.text_atlas_shader_program, cx.text_atlas_vertex_shader); + glAttachShader(cx.text_atlas_shader_program, cx.text_atlas_fragment_shader); + glLinkProgram(cx.text_atlas_shader_program); + _opengl_gfx_check_shader_program_error( + _String("failed to link text_atlas shader program"), + cx.text_atlas_shader_program, GL_LINK_STATUS); + + cx.buffers = newArray(GLuint, 8); + cx.textures = newArray(GLuint, 8); + /* ******** */ + + return cx; +} + +static _opengl_gfx_context_x11 _opengl_gfx_init_context_11(uint32_t width, + uint32_t height) { + Display *display = XOpenDisplay(NULL); + if (display == NULL) { + fprintf(stderr, "Failed to open X display\n"); + exit(1); + } + + int screen = DefaultScreen(display); + Window window = XCreateSimpleWindow( + display, RootWindow(display, screen), 0, 0, width, height, 1, + BlackPixel(display, screen), WhitePixel(display, screen)); + XSelectInput(display, window, + ExposureMask | KeyPressMask | ButtonPressMask | + ButtonReleaseMask); + XMapWindow(display, window); + + return (_opengl_gfx_context_x11){ + .display = display, + .window = window, + .screen = screen, + }; +} + +static void _opengl_gfx_send_events_wayland(_opengl_gfx_context_wayland *cx) { + wl_display_dispatch(cx->display); + + // TODO: don't just render like crazy, limit framerate + _opengl_gfx_present_wayland(cx); +} + +static void _opengl_gfx_send_events_x11(_opengl_gfx_context_x11 *cx) { + XEvent e; + XNextEvent(cx->display, &e); + if (e.type == Expose) { + XFillRectangle(cx->display, cx->window, DefaultGC(cx->display, cx->screen), + 20, 20, 10, 10); + } + + if (e.type == KeyPress) { + keep_running = false; + + XCloseDisplay(cx->display); + } +} + +static void _opengl_gfx_present_wayland(_opengl_gfx_context_wayland *cx) { + _gfx_context.gpu_glyphs.size = 0; + _gfx_context.gpu_ui_rects.size = 0; + _gfx_context.frame_func(cx->mouse_x, cx->mouse_y, cx->mouse_left_down, + cx->mouse_right_down); + + if (_gfx_context.gpu_glyphs.size > 0) { + gfx_update_buffer(&_gfx_context, 2, _gfx_context.gpu_glyphs.data, + _gfx_context.gpu_glyphs.size * sizeof(GpuGlyph)); + } + if (_gfx_context.gpu_ui_rects.size > 0) { + gfx_update_buffer(&_gfx_context, 4, _gfx_context.gpu_ui_rects.data, + _gfx_context.gpu_ui_rects.size * sizeof(GpuUiRect)); + } + + GpuUniformParams gpu_uniform_params = { + .screen_size = + { + (float)_gfx_context.frame_width, + (float)_gfx_context.frame_height, + }, + .font_size = + { + (float)_FONT_WIDTH, + (float)_FONT_HEIGHT, + }, + }; + + gfx_update_buffer(&_gfx_context, 3, &gpu_uniform_params, + sizeof(GpuUniformParams)); + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + glViewport(0, 0, _gfx_context.frame_width, _gfx_context.frame_height); + + if (_gfx_context.gpu_ui_rects.size > 0) { + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, cx->buffers.data[0]); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, cx->buffers.data[4]); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, cx->buffers.data[3]); + glUseProgram(cx->ui_rect_shader_program); + + const uint16_t indices[] = {0, 1, 2, 0, 2, 3}; + glDrawElementsInstanced(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, indices, + _gfx_context.gpu_ui_rects.size); + } + + if (_gfx_context.gpu_glyphs.size > 0) { + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, cx->buffers.data[0]); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, cx->buffers.data[2]); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, cx->buffers.data[3]); + glUseProgram(cx->text_atlas_shader_program); + + const uint16_t indices[] = {0, 1, 2, 0, 2, 3}; + glDrawElementsInstanced(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, indices, + _gfx_context.gpu_glyphs.size); + } + + glFlush(); + + if (eglSwapBuffers(cx->egl_display, cx->egl_surface) != EGL_TRUE) { + fprintf(stderr, "eglSwapBuffers() failed\n"); + } +} + +static size_t +_opengl_gfx_push_texture_buffer_wayland(_opengl_gfx_context_wayland *cx, + uint32_t width, uint32_t height, + const void *data, size_t len) { + pushArray(GLuint, &cx->textures, 0); + glCreateTextures(GL_TEXTURE_2D, 1, &cx->textures.data[cx->textures.size - 1]); + glTextureParameteri(cx->textures.data[cx->textures.size - 1], + GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTextureParameteri(cx->textures.data[cx->textures.size - 1], + GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTextureStorage2D(cx->textures.data[cx->textures.size - 1], 1, GL_RGBA8, + width, height); + glTextureSubImage2D(cx->textures.data[cx->textures.size - 1], 0, 0, 0, width, + height, GL_RED, GL_UNSIGNED_BYTE, data); + glBindTextureUnit(cx->textures.size - 1, + cx->textures.data[cx->textures.size - 1]); + + return cx->textures.size - 1; +} + +static void +_opengl_gfx_resize_texture_buffer_wayland(_opengl_gfx_context_wayland *cx, + uint32_t width, size_t texture_index, + uint32_t height) { + // TODO + assert(false && "_opengl_gfx_resize_texture_buffer_wayland unimplemented"); +} + +size_t _opengl_gfx_push_vertex_buffer_wayland(_opengl_gfx_context_wayland *cx, + const void *data, size_t len) { + pushArray(GLuint, &cx->buffers, 0); + glCreateBuffers(1, &cx->buffers.data[cx->buffers.size - 1]); + glNamedBufferStorage(cx->buffers.data[cx->buffers.size - 1], len, data, + GL_DYNAMIC_STORAGE_BIT); + + return cx->buffers.size - 1; +} +static size_t +_opengl_gfx_allocate_vertex_buffer_wayland(_opengl_gfx_context_wayland *cx, + size_t len) { + pushArray(GLuint, &cx->buffers, 0); + glCreateBuffers(1, &cx->buffers.data[cx->buffers.size - 1]); + glNamedBufferStorage(cx->buffers.data[cx->buffers.size - 1], len, (void *)0, + GL_DYNAMIC_STORAGE_BIT); + + return cx->buffers.size - 1; +} +static void _opengl_gfx_update_buffer_wayland(_opengl_gfx_context_wayland *cx, + size_t buffer_index, + const void *data, size_t len) { + glNamedBufferSubData(cx->buffers.data[buffer_index], 0, len, data); +} +#endif + +void gfx_run_events(gfx_context_t *cx) { +#if defined(__APPLE__) + return _metal_gfx_send_events(&cx->backend); +#elif __linux__ + return _opengl_gfx_send_events_wayland(&cx->backend); +#else +#error "Unsupported graphics backend" +#endif +} + +size_t gfx_push_texture_buffer(gfx_context_t *cx, uint32_t width, + uint32_t height, const void *data, size_t len) { +#if defined(__APPLE__) + return _metal_gfx_push_texture_buffer(&cx->backend, width, height, data, len); +#elif __linux__ + return _opengl_gfx_push_texture_buffer_wayland(&cx->backend, width, height, + data, len); +#else +#error "Unsupported graphics backend" +#endif +} + +size_t gfx_push_vertex_buffer(gfx_context_t *cx, const void *data, size_t len) { +#if defined(__APPLE__) + return _metal_gfx_push_vertex_buffer(&cx->backend, data, len); +#elif __linux__ + return _opengl_gfx_push_vertex_buffer_wayland(&cx->backend, data, len); +#else +#error "Unsupported graphics backend" +#endif +} + +size_t gfx_allocate_vertex_buffer(gfx_context_t *cx, size_t len) { +#if defined(__APPLE__) + return _metal_gfx_allocate_vertex_buffer(&cx->backend, len); +#elif __linux__ + return _opengl_gfx_allocate_vertex_buffer_wayland(&cx->backend, len); +#else +#error "Unsupported graphics backend" +#endif +} + +// FIXME: abstract different backends +void gfx_update_buffer(gfx_context_t *cx, size_t buffer_index, const void *data, + size_t len) { +#if defined(__APPLE__) + return _metal_gfx_update_buffer(&cx->backend, buffer_index, data, len); +#elif __linux__ + return _opengl_gfx_update_buffer_wayland(&cx->backend, buffer_index, data, + len); +#else +#error "Unsupported graphics backend" +#endif +} + +void gfx_queue_char(gfx_context_t *cx, uint8_t character, float position[2]) { + GpuGlyph glyph; + if (character >= 32) { + glyph = cx->glyph_cache.data[character - 32]; + } else { + glyph = cx->glyph_cache.data[cx->glyph_cache.size - 1]; + } + + glyph.position[0] = position[0]; + glyph.position[1] = position[1]; + + pushArray(GpuGlyph, &cx->gpu_glyphs, glyph); +} + +void gfx_queue_text(gfx_context_t *cx, string text, float position[2], + float max_x, float max_y, float color[4]) { + float x = 0; + float y = 0; + for (size_t i = 0; i < text.len; ++i) { + GpuGlyph glyph; + if (text.data[i] >= 32) { + glyph = cx->glyph_cache.data[text.data[i] - 32]; + } else { + glyph = cx->glyph_cache.data[cx->glyph_cache.size - 1]; + } + + size_t j; + float j_x = 0; + for (j = i; j < text.len; ++j) { + GpuGlyph glyph; + if (text.data[j] >= 32) { + glyph = cx->glyph_cache.data[text.data[j] - 32]; + } else { + glyph = cx->glyph_cache.data[cx->glyph_cache.size - 1]; + } + + if (text.data[j] == 32) { + break; + } + + j_x += glyph.size[0] / 2 + 4; + } + + // if (x + j_x >= max_x) { + // x = 0; + // y += 24; + // } + // if (y >= max_y) { + // break; + // } + + glyph.color[0] = color[0]; + glyph.color[1] = color[1]; + glyph.color[2] = color[2]; + glyph.color[3] = color[3]; + glyph.position[0] = x + position[0]; + glyph.position[1] = y + position[1]; + x += glyph.size[0] / 2 + 4; + + pushArray(GpuGlyph, &cx->gpu_glyphs, glyph); + } +} + +void gfx_queue_ui_rect(gfx_context_t *cx, float position[2], float size[2], + float border_size, float color[4]) { + GpuUiRect rect = + (GpuUiRect){.position = {position[0], position[1]}, + .size = {size[0], size[1]}, + .border_size = {border_size, border_size}, + .color = {color[0], color[1], color[2], color[3]}}; + + pushArray(GpuUiRect, &cx->gpu_ui_rects, rect); +} + +void gfx_add_glyph(gfx_context_t *cx, GpuGlyph glyph) { + pushArray(GpuGlyph, &cx->glyph_cache, glyph); +} + +void *gfx_init_context(_gfx_frame_func frame_func, uint32_t width, + uint32_t height) { + __auto_type backend = +#if defined(__APPLE__) + _metal_gfx_init_context(width, height); +#elif __linux__ + _opengl_gfx_init_context_wayland(width, height); +#else +#error "Unsupported graphics backend" +#endif + + _gfx_context.backend = backend; + _gfx_context.frame_func = frame_func; + _gfx_context.frame_width = width; + _gfx_context.frame_height = height; + + _gfx_context.gpu_ui_rects = newArray(GpuUiRect, 2000); + _gfx_context.gpu_glyphs = newArray(GpuGlyph, 8192 * 32); + _gfx_context.glyph_cache = newArray(GpuGlyph, 97); + + const float vertices[] = { + // positions texture coords + -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, + 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 0.0f, 1.0f, + }; + const uint16_t indices[] = {0, 1, 2, 0, 2, 3}; + + // NOTE: the order of these matter + gfx_push_vertex_buffer(&_gfx_context, vertices, sizeof(vertices)); + gfx_push_vertex_buffer(&_gfx_context, indices, sizeof(indices)); + gfx_allocate_vertex_buffer(&_gfx_context, _gfx_context.gpu_glyphs.capacity * + sizeof(GpuGlyph)); + gfx_allocate_vertex_buffer(&_gfx_context, sizeof(GpuUniformParams)); + gfx_allocate_vertex_buffer(&_gfx_context, _gfx_context.gpu_ui_rects.capacity * + sizeof(GpuUiRect)); + + return &_gfx_context; +} + +#endif +#endif diff --git a/vendor/pcleavelin/string.h b/vendor/pcleavelin/string.h new file mode 100644 index 0000000..c9601d2 --- /dev/null +++ b/vendor/pcleavelin/string.h @@ -0,0 +1,106 @@ +#ifndef ED_STRING_INCLUDED +#define ED_STRING_INCLUDED + +#include +#include +#include +#include +#include + +#define _String(text) \ + ((string){.data = (uint8_t *)text, .len = sizeof(text) - 1, .owned = false}) +#define _CString_To_String(text) \ + ((string){.data = (uint8_t *)text, .len = strlen(text), .owned = false}) +typedef struct { + uint8_t *data; + size_t len; + + // FIXME: this is so terribly bad please don't do this + bool owned; +} string; + +bool string_eq(string a, string b); +bool string_eq_cstring(string a, const char *b); +string string_copy(string s); +string string_copy_cstring(const char *str); +char *cstring_copy_string(string str); + +#ifdef ED_STRING_IMPLEMENTATION +bool string_eq(string a, string b) { + if (a.len != b.len || a.data == NULL || b.data == NULL) + return false; + + for (size_t i = 0; i < a.len; ++i) { + if (a.data[i] != b.data[i]) + return false; + } + + return true; +} + +bool string_eq_cstring(string a, const char *b) { + if (b == NULL) { + if (a.len == 0) { + return true; + } + + return false; + } + + size_t b_len = strlen(b); + if (a.len != b_len) + return false; + + for (size_t i = 0; i < a.len; ++i) { + if (a.data[i] != b[i]) + return false; + } + + return true; +} + +string string_copy(string a) { + if (a.data == NULL || a.len == 0) { + return (string){.data = NULL, .len = 0, .owned = false}; + } + + string new_string; + + new_string.data = malloc(a.len * sizeof(uint8_t)); + new_string.len = a.len; + new_string.owned = true; + + memcpy(new_string.data, a.data, new_string.len * sizeof(uint8_t)); + + return new_string; +} + +string string_copy_cstring(const char *str) { + if (str == NULL) { + return (string){.data = NULL, .len = 0, .owned = false}; + } + + string new_string; + + size_t len = strlen(str); + + new_string.data = malloc(len * sizeof(uint8_t)); + new_string.len = len; + new_string.owned = true; + + memcpy(new_string.data, str, new_string.len * sizeof(uint8_t)); + + return new_string; +} + +char *cstring_copy_string(string str) { + char *new_str = malloc(str.len + 1); + + memcpy(new_str, str.data, str.len); + new_str[str.len] = 0; + + return new_str; +} + +#endif +#endif