diff --git a/examples/custom-renderer/Game.zig b/examples/custom-renderer/Game.zig new file mode 100644 index 00000000..4d4c75a7 --- /dev/null +++ b/examples/custom-renderer/Game.zig @@ -0,0 +1,162 @@ +const std = @import("std"); +const mach = @import("mach"); +const ecs = mach.ecs; +const core = mach.core; +const math = mach.math; +const Renderer = @import("Renderer.zig"); + +const vec3 = math.vec3; +const vec2 = math.vec2; +const Vec2 = math.Vec2; +const Vec3 = math.Vec3; + +timer: mach.Timer, +player: ecs.EntityID, +direction: Vec2 = vec2(0, 0), +spawning: bool = false, +spawn_timer: mach.Timer, + +pub const components = struct { + pub const follower = void; +}; + +// Each module must have a globally unique name declared, it is impossible to use two modules with +// the same name in a program. To avoid name conflicts, we follow naming conventions: +// +// 1. `.mach` and the `.mach_foobar` namespace is reserved for Mach itself and the modules it +// provides. +// 2. Single-word names like `.renderer`, `.game`, etc. are reserved for the application itself. +// 3. Libraries which provide modules MUST be prefixed with an "owner" name, e.g. `.ziglibs_imgui` +// instead of `.imgui`. We encourage using e.g. your GitHub name, as these must be globally +// unique. +// +pub const name = .game; +pub const Mod = mach.Mod(@This()); + +pub fn init( + engine: *mach.Engine.Mod, + renderer: *Renderer.Mod, + game: *Mod, +) !void { + // The Mach .core is where we set window options, etc. + core.setTitle("Hello, ECS!"); + + // We can create entities, and set components on them. Note that components live in a module + // namespace, e.g. the `.renderer` module could have a 3D `.location` component with a different + // type than the `.physics2d` module's `.location` component if you desire. + + const player = try engine.newEntity(); + try renderer.set(player, .location, vec3(0, 0, 0)); + try renderer.set(player, .scale, 1.0); + + game.state = .{ + .timer = try mach.Timer.start(), + .spawn_timer = try mach.Timer.start(), + .player = player, + }; +} + +pub fn tick( + engine: *mach.Engine.Mod, + renderer: *Renderer.Mod, + game: *Mod, +) !void { + // TODO(engine): event polling should occur in mach.Engine module and get fired as ECS events. + var iter = core.pollEvents(); + var direction = game.state.direction; + var spawning = game.state.spawning; + while (iter.next()) |event| { + switch (event) { + .key_press => |ev| { + switch (ev.key) { + .left => direction.v[0] -= 1, + .right => direction.v[0] += 1, + .up => direction.v[1] += 1, + .down => direction.v[1] -= 1, + .space => spawning = true, + else => {}, + } + }, + .key_release => |ev| { + switch (ev.key) { + .left => direction.v[0] += 1, + .right => direction.v[0] -= 1, + .up => direction.v[1] -= 1, + .down => direction.v[1] += 1, + .space => spawning = false, + else => {}, + } + }, + .close => try engine.send(.exit, .{}), + else => {}, + } + } + game.state.direction = direction; + game.state.spawning = spawning; + + var player_pos = renderer.get(game.state.player, .location).?; + if (spawning and game.state.spawn_timer.read() > 1.0 / 60.0) { + for (0..10) |_| { + // Spawn a new follower entity + _ = game.state.spawn_timer.lap(); + const new_entity = try engine.newEntity(); + try game.set(new_entity, .follower, {}); + try renderer.set(new_entity, .location, player_pos); + try renderer.set(new_entity, .scale, 1.0 / 6.0); + } + } + + // Multiply by delta_time to ensure that movement is the same speed regardless of the frame rate. + const delta_time = game.state.timer.lap(); + + // Move following entities closer to us. + var archetypes_iter = engine.entities.query(.{ .all = &.{ + .{ .game = &.{.follower} }, + } }); + while (archetypes_iter.next()) |archetype| { + const ids = archetype.slice(.entity, .id); + const locations = archetype.slice(.renderer, .location); + for (ids, locations) |id, location| { + // Avoid other follower entities by moving away from them if they are close to us. + const close_dist = 1.0 / 15.0; + var avoidance = Vec3.splat(0); + var avoidance_div: f32 = 1.0; + var archetypes_iter_2 = engine.entities.query(.{ .all = &.{ + .{ .game = &.{.follower} }, + } }); + while (archetypes_iter_2.next()) |archetype_2| { + const other_ids = archetype_2.slice(.entity, .id); + const other_locations = archetype_2.slice(.renderer, .location); + for (other_ids, other_locations) |other_id, other_location| { + if (id == other_id) continue; + if (location.dist(&other_location) < close_dist) { + avoidance = avoidance.sub(&location.dir(&other_location, 0.0000001)); + avoidance_div += 1.0; + } + } + } + // Avoid the player + var avoid_player_multiplier: f32 = 1.0; + if (location.dist(&player_pos) < close_dist * 6.0) { + avoidance = avoidance.sub(&location.dir(&player_pos, 0.0000001)); + avoidance_div += 1.0; + avoid_player_multiplier = 4.0; + } + + // Move away from things we want to avoid + const move_speed = 1.0 * delta_time; + var new_location = location.add(&avoidance.divScalar(avoidance_div).mulScalar(move_speed * avoid_player_multiplier)); + + // Move towards the center + new_location = new_location.lerp(&vec3(0, 0, 0), move_speed / avoidance_div); + try renderer.set(id, .location, new_location); + } + } + + // Calculate the player position, by moving in the direction the player wants to go + // by the speed amount. + const speed = 1.0; + player_pos.v[0] += direction.x() * speed * delta_time; + player_pos.v[1] += direction.y() * speed * delta_time; + try renderer.set(game.state.player, .location, player_pos); +} diff --git a/examples/custom-renderer/Renderer.zig b/examples/custom-renderer/Renderer.zig new file mode 100644 index 00000000..85a3f6fc --- /dev/null +++ b/examples/custom-renderer/Renderer.zig @@ -0,0 +1,166 @@ +const std = @import("std"); + +const mach = @import("mach"); +const core = mach.core; +const gpu = mach.gpu; +const math = mach.math; + +const Vec3 = math.Vec3; + +const num_bind_groups = 1024 * 32; + +// uniform bind group offset must be 256-byte aligned +const uniform_offset = 256; + +pipeline: *gpu.RenderPipeline, +queue: *gpu.Queue, +bind_groups: [num_bind_groups]*gpu.BindGroup, +uniform_buffer: *gpu.Buffer, + +pub const name = .renderer; +pub const Mod = mach.Mod(@This()); + +pub const components = struct { + pub const location = Vec3; + pub const rotation = Vec3; + pub const scale = f32; +}; + +// TODO: this shouldn't be a packed struct, it should be extern. +const UniformBufferObject = packed struct { + offset: Vec3.Vector, + scale: f32, +}; + +pub fn init( + engine: *mach.Engine.Mod, + renderer: *Mod, +) !void { + const device = engine.state.device; + const shader_module = device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl")); + + // Fragment state + const blend = gpu.BlendState{}; + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &blend, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment = gpu.FragmentState.init(.{ + .module = shader_module, + .entry_point = "frag_main", + .targets = &.{color_target}, + }); + + const uniform_buffer = device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = @sizeOf(UniformBufferObject) * uniform_offset * num_bind_groups, + .mapped_at_creation = .false, + }); + const bind_group_layout_entry = gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true }, .uniform, true, 0); + const bind_group_layout = device.createBindGroupLayout( + &gpu.BindGroupLayout.Descriptor.init(.{ + .entries = &.{bind_group_layout_entry}, + }), + ); + var bind_groups: [num_bind_groups]*gpu.BindGroup = undefined; + for (bind_groups, 0..) |_, i| { + bind_groups[i] = device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = bind_group_layout, + .entries = &.{ + gpu.BindGroup.Entry.buffer(0, uniform_buffer, uniform_offset * i, @sizeOf(UniformBufferObject)), + }, + }), + ); + } + + const bind_group_layouts = [_]*gpu.BindGroupLayout{bind_group_layout}; + const pipeline_layout = device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{ + .bind_group_layouts = &bind_group_layouts, + })); + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .fragment = &fragment, + .layout = pipeline_layout, + .vertex = gpu.VertexState{ + .module = shader_module, + .entry_point = "vertex_main", + }, + }; + + renderer.state = .{ + .pipeline = device.createRenderPipeline(&pipeline_descriptor), + .queue = device.getQueue(), + .bind_groups = bind_groups, + .uniform_buffer = uniform_buffer, + }; + shader_module.release(); +} + +pub fn deinit( + renderer: *Mod, +) !void { + renderer.state.pipeline.release(); + renderer.state.queue.release(); + for (renderer.state.bind_groups) |bind_group| bind_group.release(); + renderer.state.uniform_buffer.release(); +} + +pub fn tick( + engine: *mach.Engine.Mod, + renderer: *Mod, +) !void { + const device = engine.state.device; + + // Begin our render pass + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = std.mem.zeroes(gpu.Color), + .load_op = .clear, + .store_op = .store, + }; + + const encoder = device.createCommandEncoder(null); + const render_pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + }); + + // Update uniform buffer + var archetypes_iter = engine.entities.query(.{ .all = &.{ + .{ .renderer = &.{ .location, .scale } }, + } }); + var num_entities: usize = 0; + while (archetypes_iter.next()) |archetype| { + const ids = archetype.slice(.entity, .id); + const locations = archetype.slice(.renderer, .location); + const scales = archetype.slice(.renderer, .scale); + for (ids, locations, scales) |id, location, scale| { + _ = id; + + const ubo = UniformBufferObject{ + .offset = location.v, + .scale = scale, + }; + encoder.writeBuffer(renderer.state.uniform_buffer, uniform_offset * num_entities, &[_]UniformBufferObject{ubo}); + num_entities += 1; + } + } + + const pass = encoder.beginRenderPass(&render_pass_info); + for (renderer.state.bind_groups[0..num_entities]) |bind_group| { + pass.setPipeline(renderer.state.pipeline); + pass.setBindGroup(0, bind_group, &.{0}); + pass.draw(3, 1, 0, 0); + } + pass.end(); + pass.release(); + + var command = encoder.finish(null); + encoder.release(); + + renderer.state.queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + core.swap_chain.present(); + back_buffer_view.release(); +} diff --git a/examples/custom-renderer/main.zig b/examples/custom-renderer/main.zig new file mode 100644 index 00000000..74c2a44f --- /dev/null +++ b/examples/custom-renderer/main.zig @@ -0,0 +1,15 @@ +// Experimental ECS app example. Not yet ready for actual use. +const mach = @import("mach"); + +const Renderer = @import("Renderer.zig"); +const Game = @import("Game.zig"); + +// The list of modules to be used in our application. Our game itself is implemented in our own +// module called Game. +pub const modules = .{ + mach.Engine, + Renderer, + Game, +}; + +pub const App = mach.App; diff --git a/examples/custom-renderer/shader.wgsl b/examples/custom-renderer/shader.wgsl new file mode 100644 index 00000000..20e08214 --- /dev/null +++ b/examples/custom-renderer/shader.wgsl @@ -0,0 +1,22 @@ +struct Uniform { + pos: vec3, + scale: f32, +}; + +@group(0) @binding(0) var in : Uniform; + +@vertex fn vertex_main( + @builtin(vertex_index) VertexIndex : u32 +) -> @builtin(position) vec4 { + var positions = array, 3>( + vec2( 0.0, 0.1), + vec2(-0.1, -0.1), + vec2( 0.1, -0.1) + ); + var pos = positions[VertexIndex]; + return vec4((pos*in.scale)+in.pos.xy, 0.0, 1.0); +} + +@fragment fn frag_main() -> @location(0) vec4 { + return vec4(1.0, 0.0, 0.0, 0.0); +} diff --git a/examples/gkurve/draw.zig b/examples/gkurve/draw.zig new file mode 100644 index 00000000..506f250b --- /dev/null +++ b/examples/gkurve/draw.zig @@ -0,0 +1,150 @@ +const std = @import("std"); + +const mach = @import("mach"); +const App = @import("main.zig").App; +const gpu = mach.gpu; +const math = mach.math; +const AtlasUV = mach.gfx.Atlas.Region.UV; + +const Mat4x4 = math.Mat4x4; +const vec3 = math.vec3; +const Vec2 = @Vector(2, f32); + +pub const Vertex = struct { + pos: @Vector(4, f32), + uv: Vec2, +}; +const VERTEX_ATTRIBUTES = [_]gpu.VertexAttribute{ + .{ .format = .float32x4, .offset = @offsetOf(Vertex, "pos"), .shader_location = 0 }, + .{ .format = .float32x2, .offset = @offsetOf(Vertex, "uv"), .shader_location = 1 }, +}; +pub const VERTEX_BUFFER_LAYOUT = gpu.VertexBufferLayout{ + .array_stride = @sizeOf(Vertex), + .step_mode = .vertex, + .attribute_count = VERTEX_ATTRIBUTES.len, + .attributes = &VERTEX_ATTRIBUTES, +}; +pub const VertexUniform = Mat4x4; + +const GkurveType = enum(u32) { + quadratic_convex = 0, + semicircle_convex = 1, + quadratic_concave = 2, + semicircle_concave = 3, + triangle = 4, +}; + +pub const FragUniform = struct { + type: GkurveType = .triangle, + // Padding for struct alignment to 16 bytes (minimum in WebGPU uniform). + padding: @Vector(3, f32) = undefined, + blend_color: @Vector(4, f32) = @Vector(4, f32){ 1, 1, 1, 1 }, +}; + +pub fn equilateralTriangle(app: *App, position: Vec2, scale: f32, uniform: FragUniform, uv: AtlasUV, height_scale: f32) !void { + const triangle_height = scale * @sqrt(0.75) * height_scale; + + try app.vertices.appendSlice(&[3]Vertex{ + .{ + .pos = .{ position[0] + scale / 2, position[1] + triangle_height, 0, 1 }, + .uv = .{ uv.x + uv.width * 0.5, uv.y + uv.height }, + }, + .{ + .pos = .{ position[0], position[1], 0, 1 }, + .uv = .{ uv.x, uv.y }, + }, + .{ + .pos = .{ position[0] + scale, position[1], 0, 1 }, + .uv = .{ uv.x + uv.width, uv.y }, + }, + }); + + try app.fragment_uniform_list.append(uniform); + + app.update_vertex_buffer = true; + app.update_frag_uniform_buffer = true; +} + +pub fn quad(app: *App, position: Vec2, scale: Vec2, uniform: FragUniform, uv: AtlasUV) !void { + const bottom_left_uv = Vec2{ uv.x, uv.y }; + const bottom_right_uv = Vec2{ uv.x + uv.width, uv.y }; + const top_left_uv = Vec2{ uv.x, uv.y + uv.height }; + const top_right_uv = Vec2{ uv.x + uv.width, uv.y + uv.height }; + + try app.vertices.appendSlice(&[6]Vertex{ + .{ .pos = .{ position[0], position[1] + scale[1], 0, 1 }, .uv = top_left_uv }, + .{ .pos = .{ position[0], position[1], 0, 1 }, .uv = bottom_left_uv }, + .{ .pos = .{ position[0] + scale[0], position[1], 0, 1 }, .uv = bottom_right_uv }, + + .{ .pos = .{ position[0] + scale[0], position[1] + scale[1], 0, 1 }, .uv = top_right_uv }, + .{ .pos = .{ position[0], position[1] + scale[1], 0, 1 }, .uv = top_left_uv }, + .{ .pos = .{ position[0] + scale[0], position[1], 0, 1 }, .uv = bottom_right_uv }, + }); + + try app.fragment_uniform_list.appendSlice(&.{ uniform, uniform }); + + app.update_vertex_buffer = true; + app.update_frag_uniform_buffer = true; +} + +pub fn circle(app: *App, position: Vec2, radius: f32, blend_color: @Vector(4, f32), uv: AtlasUV) !void { + const low_mid = Vertex{ + .pos = .{ position[0], position[1] - (radius * 2.0), 0, 1 }, + .uv = .{ uv.x + uv.width * 0.5, uv.y }, + }; + const high_mid = Vertex{ + .pos = .{ position[0], position[1] + (radius * 2.0), 0, 1 }, + .uv = .{ uv.x + uv.width * 0.5, uv.y + uv.height }, + }; + + const mid_left = Vertex{ + .pos = .{ position[0] - radius, position[1], 0, 1 }, + .uv = .{ uv.x, uv.y + uv.height * 0.5 }, + }; + const mid_right = Vertex{ + .pos = .{ position[0] + radius, position[1], 0, 1 }, + .uv = .{ uv.x + uv.width, uv.y + uv.height * 0.5 }, + }; + + try app.vertices.appendSlice(&[_]Vertex{ + high_mid, + mid_left, + mid_right, + + low_mid, + mid_left, + mid_right, + }); + + try app.fragment_uniform_list.appendSlice(&[_]FragUniform{ + .{ + .type = .semicircle_convex, + .blend_color = blend_color, + }, + .{ + .type = .semicircle_convex, + .blend_color = blend_color, + }, + }); + + app.update_vertex_buffer = true; + app.update_frag_uniform_buffer = true; +} + +pub fn getVertexUniformBufferObject() !VertexUniform { + // Note: We use window width/height here, not framebuffer width/height. + // On e.g. macOS, window size may be 640x480 while framebuffer size may be + // 1280x960 (subpixels.) Doing this lets us use a pixel, not subpixel, + // coordinate system. + const window_size = mach.core.size(); + const proj = Mat4x4.projection2D(.{ + .left = 0, + .right = @floatFromInt(window_size.width), + .bottom = 0, + .top = @floatFromInt(window_size.height), + .near = -0.1, + .far = 100, + }); + const mvp = proj.mul(&Mat4x4.translate(vec3(-1, -1, 0))); + return mvp; +} diff --git a/examples/gkurve/frag.wgsl b/examples/gkurve/frag.wgsl new file mode 100755 index 00000000..c17c3d5d --- /dev/null +++ b/examples/gkurve/frag.wgsl @@ -0,0 +1,199 @@ +struct FragUniform { + type_: u32, + padding: vec3, + blend_color: vec4, +} +@binding(1) @group(0) var ubos: array; +@binding(2) @group(0) var mySampler: sampler; +@binding(3) @group(0) var myTexture: texture_2d; + +const wireframe = false; +const antialiased = true; +const aa_px = 1.0; // pixels to consume for AA +const dist_scale_px = 300.0; // TODO: do not hard code + +@fragment fn main( + @location(0) uv: vec2, + @interpolate(linear) @location(1) bary_in: vec2, + @interpolate(flat) @location(2) triangle_index: u32, +) -> @location(0) vec4 { + // Example 1: Visualize barycentric coordinates: + // let bary = bary_in; + // return vec4(bary.x, bary.y, 0.0, 1.0); + // return vec4(0.0, bary.x, 0.0, 1.0); // [1.0 (bottom-left vertex), 0.0 (bottom-right vertex)] + // return vec4(0.0, bary.y, 0.0, 1.0); // [1.0 (bottom-left vertex), 0.0 (top-right face)] + + // Example 2: Very simple quadratic bezier + // let bary = bary_in; + // if (bary.x * bary.x - bary.y) > 0 { + // discard; + // } + // return vec4(0.0, 1.0, 0.0, 1.0); + + // Example 3: Render gkurve primitives + let inversion = select( 1.0, -1.0, ubos[triangle_index].type_ == 0u || ubos[triangle_index].type_ == 1u); + // Texture uvs + var correct_uv = uv; + correct_uv.y = 1.0 - correct_uv.y; + var color = textureSample(myTexture, mySampler, correct_uv) * ubos[triangle_index].blend_color; + + // Curve rendering + let border_color = vec4(1.0, 0.0, 0.0, 1.0); + let border_px = 30.0; + let is_semicircle = ubos[triangle_index].type_ == 1u || ubos[triangle_index].type_ == 3u; + var result = select( + curveColor(bary_in, border_px, border_color, color, inversion, is_semicircle), + color, + ubos[triangle_index].type_ == 4u, // triangle rendering + ); + + // Wireframe rendering + let wireframe_px = 1.0; + let wireframe_color = vec4(0.5, 0.5, 0.5, 1.0); + if (wireframe) { + result = wireframeColor(bary_in, wireframe_px, wireframe_color, result); + } + + if (result.a == 0.0) { discard; } + return result; +} + +// Performs alpha 'over' blending between two premultiplied-alpha colors. +fn alphaOver(a: vec4, b: vec4) -> vec4 { + return a + (b * (1.0 - a.a)); +} + +// Calculates signed distance to a quadratic bézier curve using barycentric coordinates. +fn distanceToQuadratic(bary: vec2) -> f32 { + // Gradients + let px = dpdx(bary.xy); + let py = dpdy(bary.xy); + + // Chain rule + let fx = (2.0 * bary.x) * px.x - px.y; + let fy = (2.0 * bary.x) * py.x - py.y; + + return (bary.x * bary.x - bary.y) / sqrt(fx * fx + fy * fy); +} + +// Calculates signed distance to a semicircle using barycentric coordinates. +fn distanceToSemicircle(bary: vec2) -> f32 { + let x = abs(((bary.x - 0.5) * 2.0)); // [0.0 left, 1.0 center, 0.0 right] + let y = ((bary.x-bary.y) * 4.0); // [2.0 bottom, 0.0 top] + let c = x*x + y*y; + + // Gradients + let px = dpdx(bary.xy); + let py = dpdy(bary.xy); + + // Chain rule + let fx = c * px.x - px.y; + let fy = c * py.x - py.y; + + let d = (1.0 - (x*x + y*y)) - 0.2; + return (-d / 6.0) / sqrt(fx * fx + fy * fy); +} + +// Calculates signed distance to the wireframe (i.e. faces) of the triangle using barycentric +// coordinates. +fn distanceToWireframe(bary: vec2) -> f32 { + let normal = vec3( + bary.y, // distance to right face + (bary.x - bary.y) * 2.0, // distance to bottom face + 1.0 - (((bary.x - bary.y)) + bary.x), // distance to left face + ); + let fw = sqrt(dpdx(normal)*dpdx(normal) + dpdy(normal)*dpdy(normal)); + let d = normal / fw; + return min(min(d.x, d.y), d.z); +} + +// Calculates the color of the wireframe, taking into account antialiasing and alpha blending with +// the desired background blend color. +fn wireframeColor(bary: vec2, px: f32, color: vec4, blend_color: vec4) -> vec4 { + let dist = distanceToWireframe(bary); + if (antialiased) { + let outer = dist; + let inner = (px + (aa_px * 2.0)) - dist; + let in_wireframe = outer >= 0.0 && inner >= 0.0; + if (in_wireframe) { + // Note: If this is the outer edge of the wireframe, we do not want to perform alpha + // blending with the background blend color, since it is an antialiased edge and should + // be transparent. However, if it is the internal edge of the wireframe, we do want to + // perform alpha blending as it should be an overlay, not transparent. + let is_outer_edge = outer < inner; + if (is_outer_edge) { + let alpha = smoothstep(0.0, 1.0, outer*(1.0 / aa_px)); + return vec4((color.rgb/color.a)*alpha, alpha); + } else { + let aa_inner = inner - aa_px; + let alpha = smoothstep(0.0, 1.0, aa_inner*(1.0 / aa_px)); + let wireframe_color = vec4((color.rgb/color.a)*alpha, alpha); + return alphaOver(wireframe_color, blend_color); + } + } + return blend_color; + } else { + // If we're at the edge use the wireframe color, otherwise use the background blend_color. + return select(blend_color, color, (px - dist) >= 0.0); + } +} + +// Calculates the color for a curve, taking into account antialiasing and alpha blending with +// the desired background blend color. +// +// inversion: concave (-1.0) or convex (1.0) +// is_semicircle: quadratic bezier (false) or semicircle (true) +fn curveColor( + bary: vec2, + border_px: f32, + border_color: vec4, + blend_color: vec4, + inversion: f32, + is_semicircle: bool, +) -> vec4 { + let dist = select( + distanceToQuadratic(bary), + distanceToSemicircle(bary), + is_semicircle, + ) * inversion; + let is_inverted = (inversion + 1.0) / 2.0; // 1.0 if inverted, 0.0 otherwise + + if (antialiased) { + let outer = dist + ((border_px + (aa_px * 2.0)) * is_inverted); // bottom + let inner = ((border_px + (aa_px * 2.0)) * (1.0-is_inverted)) - dist; // top + let in_border = outer >= 0.0 && inner >= 0.0; + if (in_border) { + // Note: If this is the outer edge of the curve, we do not want to perform alpha + // blending with the background blend color, since it is an antialiased edge and should + // be transparent. However, if it is the internal edge of the curve, we do want to + // perform alpha blending as it should be an overlay, not transparent. + let is_outer_edge = outer < inner; + if (is_outer_edge) { + let aa_outer = outer - (aa_px * is_inverted); + let alpha = smoothstep(0.0, 1.0, aa_outer*(1.0 / aa_px)); + return vec4((border_color.rgb/border_color.a)*alpha, alpha); + } else { + let aa_inner = inner - (aa_px * (1.0 - is_inverted)); + let alpha = smoothstep(0.0, 1.0, aa_inner*(1.0 / aa_px)); + let new_border_color = vec4((border_color.rgb/border_color.a)*alpha, alpha); + return alphaOver(new_border_color, blend_color); + } + return border_color; + } else if (outer >= 0.0) { + return blend_color; + } else { + return vec4(0.0); + } + } else { + let outer = dist + (border_px * is_inverted); + let inner = (border_px * (1.0-is_inverted)) - dist; + let in_border = outer >= 0.0 && inner >= 0.0; + if (in_border) { + return border_color; + } else if (outer >= 0.0) { + return blend_color; + } else { + return vec4(0.0); + } + } +} \ No newline at end of file diff --git a/examples/gkurve/label.zig b/examples/gkurve/label.zig new file mode 100644 index 00000000..880951c3 --- /dev/null +++ b/examples/gkurve/label.zig @@ -0,0 +1,146 @@ +//! At the moment we use only rgba32, but maybe it could be useful to use also other types + +const std = @import("std"); +const mach = @import("mach"); +const ft = @import("freetype"); +const zigimg = @import("zigimg"); +const Atlas = mach.gfx.Atlas; +const AtlasErr = Atlas.Error; +const AtlasUV = Atlas.Region.UV; +const App = @import("main.zig").App; +const draw = @import("draw.zig"); + +pub const Label = @This(); + +const Vec2 = @Vector(2, f32); +const Vec4 = @Vector(4, f32); + +const GlyphInfo = struct { + uv_data: AtlasUV, + metrics: ft.GlyphMetrics, +}; + +face: ft.Face, +size: i32, +char_map: std.AutoHashMap(u21, GlyphInfo), +allocator: std.mem.Allocator, + +const WriterContext = struct { + label: *Label, + app: *App, + position: Vec2, + text_color: Vec4, +}; +const WriterError = ft.Error || std.mem.Allocator.Error || AtlasErr; +const Writer = std.io.Writer(WriterContext, WriterError, write); + +pub fn writer(label: *Label, app: *App, position: Vec2, text_color: Vec4) Writer { + return Writer{ + .context = .{ + .label = label, + .app = app, + .position = position, + .text_color = text_color, + }, + }; +} + +pub fn init(lib: ft.Library, font_path: [*:0]const u8, face_index: i32, char_size: i32, allocator: std.mem.Allocator) !Label { + return Label{ + .face = try lib.createFace(font_path, face_index), + .size = char_size, + .char_map = std.AutoHashMap(u21, GlyphInfo).init(allocator), + .allocator = allocator, + }; +} + +pub fn deinit(label: *Label) void { + label.face.deinit(); + label.char_map.deinit(); +} + +fn write(ctx: WriterContext, bytes: []const u8) WriterError!usize { + var offset = Vec2{ 0, 0 }; + var j: usize = 0; + while (j < bytes.len) { + const len = std.unicode.utf8ByteSequenceLength(bytes[j]) catch unreachable; + const char = std.unicode.utf8Decode(bytes[j..(j + len)]) catch unreachable; + j += len; + switch (char) { + '\n' => { + offset[0] = 0; + offset[1] -= @as(f32, @floatFromInt(ctx.label.face.size().metrics().height >> 6)); + }, + ' ' => { + const v = try ctx.label.char_map.getOrPut(char); + if (!v.found_existing) { + try ctx.label.face.setCharSize(ctx.label.size * 64, 0, 50, 0); + try ctx.label.face.loadChar(char, .{ .render = true }); + const glyph = ctx.label.face.glyph(); + v.value_ptr.* = GlyphInfo{ + .uv_data = undefined, + .metrics = glyph.metrics(), + }; + } + offset[0] += @as(f32, @floatFromInt(v.value_ptr.metrics.horiAdvance >> 6)); + }, + else => { + const v = try ctx.label.char_map.getOrPut(char); + if (!v.found_existing) { + try ctx.label.face.setCharSize(ctx.label.size * 64, 0, 50, 0); + try ctx.label.face.loadChar(char, .{ .render = true }); + const glyph = ctx.label.face.glyph(); + const glyph_bitmap = glyph.bitmap(); + const glyph_width = glyph_bitmap.width(); + const glyph_height = glyph_bitmap.rows(); + + // Add 1 pixel padding to texture to avoid bleeding over other textures + const glyph_data = try ctx.label.allocator.alloc(zigimg.color.Rgba32, (glyph_width + 2) * (glyph_height + 2)); + defer ctx.label.allocator.free(glyph_data); + const glyph_buffer = glyph_bitmap.buffer().?; + for (glyph_data, 0..) |*data, i| { + const x = i % (glyph_width + 2); + const y = i / (glyph_width + 2); + + // zig fmt: off + const glyph_col = + if (x == 0 or x == (glyph_width + 1) or y == 0 or y == (glyph_height + 1)) + 0 + else + glyph_buffer[(y - 1) * glyph_width + (x - 1)]; + // zig fmt: on + + data.* = zigimg.color.Rgba32.initRgb(glyph_col, glyph_col, glyph_col); + } + var glyph_atlas_region = try ctx.app.texture_atlas_data.reserve(ctx.label.allocator, glyph_width + 2, glyph_height + 2); + ctx.app.texture_atlas_data.set(glyph_atlas_region, @as([*]const u8, @ptrCast(glyph_data.ptr))[0 .. glyph_data.len * 4]); + + glyph_atlas_region.x += 1; + glyph_atlas_region.y += 1; + glyph_atlas_region.width -= 2; + glyph_atlas_region.height -= 2; + + v.value_ptr.* = GlyphInfo{ + .uv_data = glyph_atlas_region.calculateUV(ctx.app.texture_atlas_data.size), + .metrics = glyph.metrics(), + }; + } + + try draw.quad( + ctx.app, + ctx.position + offset + Vec2{ @as(f32, @floatFromInt(v.value_ptr.metrics.horiBearingX >> 6)), @as(f32, @floatFromInt((v.value_ptr.metrics.horiBearingY - v.value_ptr.metrics.height) >> 6)) }, + .{ @as(f32, @floatFromInt(v.value_ptr.metrics.width >> 6)), @as(f32, @floatFromInt(v.value_ptr.metrics.height >> 6)) }, + .{ .blend_color = ctx.text_color }, + v.value_ptr.uv_data, + ); + offset[0] += @as(f32, @floatFromInt(v.value_ptr.metrics.horiAdvance >> 6)); + }, + } + } + return bytes.len; +} + +pub fn print(label: *Label, app: *App, comptime fmt: []const u8, args: anytype, position: Vec2, text_color: Vec4) !void { + const w = writer(label, app, position, text_color); + try w.print(fmt, args); +} diff --git a/examples/gkurve/main.zig b/examples/gkurve/main.zig new file mode 100644 index 00000000..1be55572 --- /dev/null +++ b/examples/gkurve/main.zig @@ -0,0 +1,339 @@ +// TODO: +// - handle textures better with texture atlas +// - handle adding and removing triangles and quads better + +const std = @import("std"); +const mach = @import("mach"); +const core = mach.core; +const ft = @import("freetype"); +const zigimg = @import("zigimg"); +const assets = @import("assets"); +const draw = @import("draw.zig"); +const Label = @import("label.zig"); +const ResizableLabel = @import("resizable_label.zig"); +const gpu = mach.gpu; +const Atlas = mach.gfx.Atlas; + +pub const App = @This(); + +pipeline: *gpu.RenderPipeline, +vertex_buffer: *gpu.Buffer, +vertices: std.ArrayList(draw.Vertex), +update_vertex_buffer: bool, +vertex_uniform_buffer: *gpu.Buffer, +update_vertex_uniform_buffer: bool, +frag_uniform_buffer: *gpu.Buffer, +fragment_uniform_list: std.ArrayList(draw.FragUniform), +update_frag_uniform_buffer: bool, +bind_group: *gpu.BindGroup, +texture_atlas_data: Atlas, + +pub fn init(app: *App) !void { + try core.init(.{}); + + // TODO: Refactor texture atlas size number + app.texture_atlas_data = try Atlas.init( + core.allocator, + 1280, + .rgba, + ); + const atlas_size = gpu.Extent3D{ .width = app.texture_atlas_data.size, .height = app.texture_atlas_data.size }; + + const texture = core.device.createTexture(&.{ + .size = atlas_size, + .format = .rgba8_unorm, + .usage = .{ + .texture_binding = true, + .copy_dst = true, + .render_attachment = true, + }, + }); + const data_layout = gpu.Texture.DataLayout{ + .bytes_per_row = @as(u32, @intCast(atlas_size.width * 4)), + .rows_per_image = @as(u32, @intCast(atlas_size.height)), + }; + + var img = try zigimg.Image.fromMemory(core.allocator, assets.gotta_go_fast_png); + defer img.deinit(); + + const atlas_img_region = try app.texture_atlas_data.reserve(core.allocator, @as(u32, @truncate(img.width)), @as(u32, @truncate(img.height))); + const img_uv_data = atlas_img_region.calculateUV(app.texture_atlas_data.size); + + switch (img.pixels) { + .rgba32 => |pixels| app.texture_atlas_data.set( + atlas_img_region, + @as([*]const u8, @ptrCast(pixels.ptr))[0 .. pixels.len * 4], + ), + .rgb24 => |pixels| { + const data = try rgb24ToRgba32(core.allocator, pixels); + defer data.deinit(core.allocator); + app.texture_atlas_data.set( + atlas_img_region, + @as([*]const u8, @ptrCast(data.rgba32.ptr))[0 .. data.rgba32.len * 4], + ); + }, + else => @panic("unsupported image color format"), + } + + const white_tex_scale = 80; + var atlas_white_region = try app.texture_atlas_data.reserve(core.allocator, white_tex_scale, white_tex_scale); + atlas_white_region.x += 1; + atlas_white_region.y += 1; + atlas_white_region.width -= 2; + atlas_white_region.height -= 2; + const white_texture_uv_data = atlas_white_region.calculateUV(app.texture_atlas_data.size); + const white_tex_data = try core.allocator.alloc(zigimg.color.Rgba32, white_tex_scale * white_tex_scale); + defer core.allocator.free(white_tex_data); + @memset(white_tex_data, zigimg.color.Rgba32.initRgb(0xff, 0xff, 0xff)); + app.texture_atlas_data.set(atlas_white_region, @as([*]const u8, @ptrCast(white_tex_data.ptr))[0 .. white_tex_data.len * 4]); + + app.vertices = try std.ArrayList(draw.Vertex).initCapacity(core.allocator, 9); + app.fragment_uniform_list = try std.ArrayList(draw.FragUniform).initCapacity(core.allocator, 3); + + // Quick test for using freetype + const lib = try ft.Library.init(); + defer lib.deinit(); + + const DemoMode = enum { + gkurves, + bitmap_text, // TODO: broken + text, + quad, + circle, + }; + const demo_mode: DemoMode = .gkurves; + + core.queue.writeTexture( + &.{ .texture = texture }, + &data_layout, + &.{ .width = app.texture_atlas_data.size, .height = app.texture_atlas_data.size }, + app.texture_atlas_data.data, + ); + + const wsize = core.size(); + const window_width = @as(f32, @floatFromInt(wsize.width)); + const window_height = @as(f32, @floatFromInt(wsize.height)); + const triangle_scale = 250; + switch (demo_mode) { + .gkurves => { + try draw.equilateralTriangle(app, .{ window_width / 2, window_height / 1.9 }, triangle_scale, .{}, img_uv_data, 1.0); + try draw.equilateralTriangle(app, .{ window_width / 2, window_height / 1.9 - triangle_scale }, triangle_scale, .{ .type = .quadratic_concave }, img_uv_data, 1.0); + try draw.equilateralTriangle(app, .{ window_width / 2 - triangle_scale, window_height / 1.9 }, triangle_scale, .{ .type = .quadratic_convex }, white_texture_uv_data, 1.0); + try draw.equilateralTriangle(app, .{ window_width / 2 - triangle_scale, window_height / 1.9 - triangle_scale }, triangle_scale, .{ .type = .quadratic_convex }, white_texture_uv_data, 0.5); + }, + else => @panic("disabled for now"), + // TODO: disabled for now because these rely on a Label API that expects a font filepath, + // rather than bytes. This gkurve example / experiment test bed should probably be moved + // elsewhere anyway. + // + // .bitmap_text => { + // // const character = "Gotta-go-fast!\n0123456789\n~!@#$%^&*()_+è\n:\"<>?`-=[];',./"; + // // const character = "ABCDEFGHIJ\nKLMNOPQRST\nUVWXYZ"; + // const size_multiplier = 5; + // var label = try Label.init(lib, assets.roboto_medium_ttf.path, 0, 110 * size_multiplier, core.allocator); + // defer label.deinit(); + // try label.print(app, "All your game's bases are belong to us èçòà", .{}, @Vector(2, f32){ 0, 420 }, @Vector(4, f32){ 1, 1, 1, 1 }); + // try label.print(app, "wow!", .{}, @Vector(2, f32){ 70 * size_multiplier, 70 }, @Vector(4, f32){ 1, 1, 1, 1 }); + // }, + // .text => { + // const character = "Gotta-go-fast!\n0123456789\n~!@#$%^&*()_+è\n:\"<>?`-=[];',./"; + // const size_multiplier = 5; + + // var resizable_label: ResizableLabel = undefined; + // try resizable_label.init(lib, assets.roboto_medium_ttf.path, 0, core.allocator, white_texture_uv_data); + // defer resizable_label.deinit(); + // try resizable_label.print(app, character, .{}, @Vector(4, f32){ 20, 300, 0, 0 }, @Vector(4, f32){ 1, 1, 1, 1 }, 20 * size_multiplier); + // // try resizable_label.print(app, "@", .{}, @Vector(4, f32){ 20, 150, 0, 0 }, @Vector(4, f32){ 1, 1, 1, 1 }, 130 * size_multiplier); + // }, + .quad => { + try draw.quad(app, .{ 0, 0 }, .{ 480, 480 }, .{}, .{ .x = 0, .y = 0, .width = 1, .height = 1 }); + }, + .circle => { + try draw.circle(app, .{ window_width / 2, window_height / 2 }, window_height / 2 - 10, .{ 0, 0.5, 0.75, 1.0 }, white_texture_uv_data); + }, + } + + const vs_module = core.device.createShaderModuleWGSL("vert", @embedFile("vert.wgsl")); + const fs_module = core.device.createShaderModuleWGSL("frag", @embedFile("frag.wgsl")); + + const blend = gpu.BlendState{ + .color = .{ + .operation = .add, + .src_factor = .src_alpha, + .dst_factor = .one_minus_src_alpha, + }, + .alpha = .{ + .operation = .add, + .src_factor = .one, + .dst_factor = .zero, + }, + }; + + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &blend, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment = gpu.FragmentState.init(.{ + .module = fs_module, + .entry_point = "main", + .targets = &.{color_target}, + }); + + const vbgle = gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true }, .uniform, true, 0); + const fbgle = gpu.BindGroupLayout.Entry.buffer(1, .{ .fragment = true }, .read_only_storage, true, 0); + const sbgle = gpu.BindGroupLayout.Entry.sampler(2, .{ .fragment = true }, .filtering); + const tbgle = gpu.BindGroupLayout.Entry.texture(3, .{ .fragment = true }, .float, .dimension_2d, false); + const bgl = core.device.createBindGroupLayout( + &gpu.BindGroupLayout.Descriptor.init(.{ + .entries = &.{ vbgle, fbgle, sbgle, tbgle }, + }), + ); + const bind_group_layouts = [_]*gpu.BindGroupLayout{bgl}; + const pipeline_layout = core.device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{ + .bind_group_layouts = &bind_group_layouts, + })); + + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .fragment = &fragment, + .layout = pipeline_layout, + .vertex = gpu.VertexState.init(.{ + .module = vs_module, + .entry_point = "main", + .buffers = &.{draw.VERTEX_BUFFER_LAYOUT}, + }), + }; + + const vertex_buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .vertex = true }, + .size = @sizeOf(draw.Vertex) * app.vertices.items.len, + .mapped_at_creation = .false, + }); + + const vertex_uniform_buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = @sizeOf(draw.VertexUniform), + .mapped_at_creation = .false, + }); + + const frag_uniform_buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .storage = true }, + .size = @sizeOf(draw.FragUniform) * app.fragment_uniform_list.items.len, + .mapped_at_creation = .false, + }); + + const sampler = core.device.createSampler(&.{ + // .mag_filter = .linear, + // .min_filter = .linear, + }); + + std.debug.assert((app.vertices.items.len / 3) == app.fragment_uniform_list.items.len); + const bind_group = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = bgl, + .entries = &.{ + gpu.BindGroup.Entry.buffer(0, vertex_uniform_buffer, 0, @sizeOf(draw.VertexUniform)), + gpu.BindGroup.Entry.buffer(1, frag_uniform_buffer, 0, @sizeOf(draw.FragUniform) * app.fragment_uniform_list.items.len), + gpu.BindGroup.Entry.sampler(2, sampler), + gpu.BindGroup.Entry.textureView(3, texture.createView(&gpu.TextureView.Descriptor{ .dimension = .dimension_2d })), + }, + }), + ); + + app.pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + app.vertex_buffer = vertex_buffer; + app.vertex_uniform_buffer = vertex_uniform_buffer; + app.frag_uniform_buffer = frag_uniform_buffer; + app.bind_group = bind_group; + app.update_vertex_buffer = true; + app.update_vertex_uniform_buffer = true; + app.update_frag_uniform_buffer = true; + + vs_module.release(); + fs_module.release(); + pipeline_layout.release(); + bgl.release(); +} + +pub fn deinit(app: *App) void { + defer core.deinit(); + + app.vertex_buffer.release(); + app.vertex_uniform_buffer.release(); + app.frag_uniform_buffer.release(); + app.bind_group.release(); + app.vertices.deinit(); + app.fragment_uniform_list.deinit(); + app.texture_atlas_data.deinit(core.allocator); +} + +pub fn update(app: *App) !bool { + var iter = core.pollEvents(); + while (iter.next()) |event| { + switch (event) { + .key_press => |ev| { + if (ev.key == .space) return true; + }, + .framebuffer_resize => { + app.update_vertex_uniform_buffer = true; + }, + .close => return true, + else => {}, + } + } + + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = std.mem.zeroes(gpu.Color), + .load_op = .clear, + .store_op = .store, + }; + + const encoder = core.device.createCommandEncoder(null); + const render_pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + }); + + { + if (app.update_vertex_buffer) { + encoder.writeBuffer(app.vertex_buffer, 0, app.vertices.items); + app.update_vertex_buffer = false; + } + if (app.update_frag_uniform_buffer) { + encoder.writeBuffer(app.frag_uniform_buffer, 0, app.fragment_uniform_list.items); + app.update_frag_uniform_buffer = false; + } + if (app.update_vertex_uniform_buffer) { + encoder.writeBuffer(app.vertex_uniform_buffer, 0, &[_]draw.VertexUniform{try draw.getVertexUniformBufferObject()}); + app.update_vertex_uniform_buffer = false; + } + } + + const pass = encoder.beginRenderPass(&render_pass_info); + pass.setPipeline(app.pipeline); + pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(draw.Vertex) * app.vertices.items.len); + pass.setBindGroup(0, app.bind_group, &.{ 0, 0 }); + pass.draw(@as(u32, @truncate(app.vertices.items.len)), 1, 0, 0); + pass.end(); + pass.release(); + + var command = encoder.finish(null); + encoder.release(); + + core.queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + core.swap_chain.present(); + back_buffer_view.release(); + + return false; +} + +fn rgb24ToRgba32(allocator: std.mem.Allocator, in: []zigimg.color.Rgb24) !zigimg.color.PixelStorage { + const out = try zigimg.color.PixelStorage.init(allocator, .rgba32, in.len); + var i: usize = 0; + while (i < in.len) : (i += 1) { + out.rgba32[i] = zigimg.color.Rgba32{ .r = in[i].r, .g = in[i].g, .b = in[i].b, .a = 255 }; + } + return out; +} diff --git a/examples/gkurve/resizable_label.zig b/examples/gkurve/resizable_label.zig new file mode 100644 index 00000000..8d6e2c71 --- /dev/null +++ b/examples/gkurve/resizable_label.zig @@ -0,0 +1,555 @@ +//! TODO: Refactor the API, maybe use a handle that contains the lib and other things and controls init and deinit of ft.Lib and other things + +const std = @import("std"); +const mach = @import("mach"); +const ft = @import("freetype"); +const App = @import("main.zig").App; +const Vertex = @import("draw.zig").Vertex; +const math = mach.math; +const earcut = mach.earcut; +const Atlas = mach.gfx.Atlas; +const AtlasErr = Atlas.Error; +const AtlasUV = Atlas.Region.UV; + +// If true, show the filled triangles green, the concave beziers blue and the convex ones red +const debug_colors = false; + +pub const ResizableLabel = @This(); + +const Vec2 = @Vector(2, f32); +const Vec4 = @Vector(4, f32); +const VertexList = std.ArrayList(Vertex); + +// All the data that a single character needs to be rendered +// TODO: hori/vert advance, write file format +const CharVertices = struct { + filled_vertices: VertexList, + filled_vertices_indices: std.ArrayList(u16), + // Concave vertices belong to the filled_vertices list, so just index them + concave_vertices: std.ArrayList(u16), + // The point outside of the convex bezier, doesn't belong to the filled vertices, + // But the other two points do, so put those in the indices + convex_vertices: VertexList, + convex_vertices_indices: std.ArrayList(u16), + + fn deinit(self: CharVertices) void { + self.filled_vertices.deinit(); + self.filled_vertices_indices.deinit(); + self.concave_vertices.deinit(); + self.convex_vertices.deinit(); + self.convex_vertices_indices.deinit(); + } +}; + +face: ft.Face, +char_map: std.AutoHashMap(u21, CharVertices), +allocator: std.mem.Allocator, +tessellator: earcut.Processor(f32), +white_texture: AtlasUV, + +// The data that the write function needs +// TODO: move twxture here, don't limit to just white_texture +const WriterContext = struct { + label: *ResizableLabel, + app: *App, + position: Vec4, + text_color: Vec4, + text_size: u32, +}; +const WriterError = ft.Error || std.mem.Allocator.Error || AtlasErr; +const Writer = std.io.Writer(WriterContext, WriterError, write); + +pub fn writer(label: *ResizableLabel, app: *App, position: Vec4, text_color: Vec4, text_size: u32) Writer { + return Writer{ + .context = .{ + .label = label, + .app = app, + .position = position, + .text_color = text_color, + .text_size = text_size, + }, + }; +} + +pub fn init(self: *ResizableLabel, lib: ft.Library, font_path: [*:0]const u8, face_index: i32, allocator: std.mem.Allocator, white_texture: AtlasUV) !void { + self.* = ResizableLabel{ + .face = try lib.createFace(font_path, face_index), + .char_map = std.AutoHashMap(u21, CharVertices).init(allocator), + .allocator = allocator, + .tessellator = earcut.Processor(f32){}, + .white_texture = white_texture, + }; +} + +pub fn deinit(label: *ResizableLabel) void { + label.face.deinit(); + label.tessellator.deinit(label.allocator); + + var iter = label.char_map.valueIterator(); + while (iter.next()) |ptr| { + ptr.deinit(); + } + + label.char_map.deinit(); +} + +// TODO: handle offsets +// FIXME: many useless allocations for the arraylists +fn write(ctx: WriterContext, bytes: []const u8) WriterError!usize { + var offset = Vec4{ 0, 0, 0, 0 }; + var c: usize = 0; + while (c < bytes.len) { + const len = std.unicode.utf8ByteSequenceLength(bytes[c]) catch unreachable; + const char = std.unicode.utf8Decode(bytes[c..(c + len)]) catch unreachable; + c += len; + switch (char) { + '\n' => { + offset[0] = 0; + offset[1] -= @as(f32, @floatFromInt(ctx.label.face.glyph().metrics().vertAdvance)) * (@as(f32, @floatFromInt(ctx.text_size)) / 1024); + }, + ' ' => { + @panic("TODO: Space character not implemented yet"); + // const v = try ctx.label.char_map.getOrPut(char); + // if (!v.found_existing) { + // try ctx.label.face.setCharSize(ctx.label.size * 64, 0, 50, 0); + // try ctx.label.face.loadChar(char, .{ .render = true }); + // const glyph = ctx.label.face.glyph; + // v.value_ptr.* = GlyphInfo{ + // .uv_data = undefined, + // .metrics = glyph.metrics(), + // }; + // } + // offset[0] += @intToFloat(f32, v.value_ptr.metrics.horiAdvance >> 6); + }, + else => { + const v = try ctx.label.char_map.getOrPut(char); + if (!v.found_existing) { + try ctx.label.face.loadChar(char, .{ .no_scale = true, .no_bitmap = true }); + const glyph = ctx.label.face.glyph(); + + // Use a big scale and then scale to the actual text size + const multiplier = 1024 << 6; + const matrix = ft.Matrix{ + .xx = 1 * multiplier, + .xy = 0 * multiplier, + .yx = 0 * multiplier, + .yy = 1 * multiplier, + }; + glyph.outline().?.transform(matrix); + + v.value_ptr.* = CharVertices{ + .filled_vertices = VertexList.init(ctx.label.allocator), + .filled_vertices_indices = std.ArrayList(u16).init(ctx.label.allocator), + .concave_vertices = std.ArrayList(u16).init(ctx.label.allocator), + .convex_vertices = VertexList.init(ctx.label.allocator), + .convex_vertices_indices = std.ArrayList(u16).init(ctx.label.allocator), + }; + + var outline_ctx = OutlineContext{ + .outline_verts = std.ArrayList(std.ArrayList(Vec2)).init(ctx.label.allocator), + .inside_verts = std.ArrayList(Vec2).init(ctx.label.allocator), + .concave_vertices = std.ArrayList(Vec2).init(ctx.label.allocator), + .convex_vertices = std.ArrayList(Vec2).init(ctx.label.allocator), + }; + defer outline_ctx.outline_verts.deinit(); + defer { + for (outline_ctx.outline_verts.items) |*item| item.deinit(); + } + defer outline_ctx.inside_verts.deinit(); + defer outline_ctx.concave_vertices.deinit(); + defer outline_ctx.convex_vertices.deinit(); + + const callbacks = ft.Outline.Funcs(*OutlineContext){ + .move_to = moveToFunction, + .line_to = lineToFunction, + .conic_to = conicToFunction, + .cubic_to = cubicToFunction, + .shift = 0, + .delta = 0, + }; + try ctx.label.face.glyph().outline().?.decompose(&outline_ctx, callbacks); + uniteOutsideAndInsideVertices(&outline_ctx); + + // Tessellator.triangulatePolygons() doesn't seem to work, so just + // call triangulatePolygon() for each polygon, and put the results all + // in all_outlines and all_indices + var all_outlines = std.ArrayList(Vec2).init(ctx.label.allocator); + defer all_outlines.deinit(); + var all_indices = std.ArrayList(u16).init(ctx.label.allocator); + defer all_indices.deinit(); + var idx_offset: u16 = 0; + for (outline_ctx.outline_verts.items) |item| { + if (item.items.len == 0) continue; + // TODO(gkurve): don't discard this, make tessellator use Vec2 / avoid copy? + var polygon = std.ArrayListUnmanaged(f32){}; + defer polygon.deinit(ctx.label.allocator); + if (ctx.label.face.glyph().outline().?.orientation() == .truetype) { + // TrueType orientation has clockwise contours, so reverse the list + // since we need CCW. + var i = item.items.len - 1; + while (i > 0) : (i -= 1) { + try polygon.append(ctx.label.allocator, item.items[i][0]); + try polygon.append(ctx.label.allocator, item.items[i][1]); + } + } else { + for (item.items) |vert| { + try polygon.append(ctx.label.allocator, vert[0]); + try polygon.append(ctx.label.allocator, vert[1]); + } + } + + try ctx.label.tessellator.process(ctx.label.allocator, polygon.items, null, 2); + + for (ctx.label.tessellator.triangles.items) |idx| { + try all_outlines.append(Vec2{ polygon.items[idx * 2], polygon.items[(idx * 2) + 1] }); + try all_indices.append(@as(u16, @intCast((idx * 2) + idx_offset))); + } + idx_offset += @as(u16, @intCast(ctx.label.tessellator.triangles.items.len)); + } + + for (all_outlines.items) |item| { + // FIXME: The uv_data is wrong, should be pushed up by the lowest a character can be + const vertex_uv = item / math.vec.splat(@Vector(2, f32), 1024 << 6); + const vertex_pos = Vec4{ item[0], item[1], 0, 1 }; + try v.value_ptr.filled_vertices.append(Vertex{ .pos = vertex_pos, .uv = vertex_uv }); + } + try v.value_ptr.filled_vertices_indices.appendSlice(all_indices.items); + + // TODO(gkurve): could more optimally find index (e.g. already know it from + // data structure, instead of finding equal point.) + for (outline_ctx.concave_vertices.items) |concave_control| { + for (all_outlines.items, 0..) |item, j| { + if (vec2Equal(item, concave_control)) { + try v.value_ptr.concave_vertices.append(@as(u16, @truncate(j))); + break; + } + } + } + + std.debug.assert((outline_ctx.convex_vertices.items.len % 3) == 0); + var i: usize = 0; + while (i < outline_ctx.convex_vertices.items.len) : (i += 3) { + const vert = outline_ctx.convex_vertices.items[i]; + const vertex_uv = vert / math.vec.splat(@Vector(2, f32), 1024 << 6); + const vertex_pos = Vec4{ vert[0], vert[1], 0, 1 }; + try v.value_ptr.convex_vertices.append(Vertex{ .pos = vertex_pos, .uv = vertex_uv }); + + var found: usize = 0; + for (all_outlines.items, 0..) |item, j| { + if (vec2Equal(item, outline_ctx.convex_vertices.items[i + 1])) { + try v.value_ptr.convex_vertices_indices.append(@as(u16, @truncate(j))); + found += 1; + } + if (vec2Equal(item, outline_ctx.convex_vertices.items[i + 2])) { + try v.value_ptr.convex_vertices_indices.append(@as(u16, @truncate(j))); + found += 1; + } + if (found == 2) break; + } + std.debug.assert(found == 2); + } + std.debug.assert(((v.value_ptr.convex_vertices.items.len + v.value_ptr.convex_vertices_indices.items.len) % 3) == 0); + } + + // Read the data and apply resizing of pos and uv + const filled_vertices_after_offset = try ctx.label.allocator.alloc(Vertex, v.value_ptr.filled_vertices.items.len); + defer ctx.label.allocator.free(filled_vertices_after_offset); + for (filled_vertices_after_offset, 0..) |*vert, i| { + vert.* = v.value_ptr.filled_vertices.items[i]; + vert.pos *= Vec4{ @as(f32, @floatFromInt(ctx.text_size)) / 1024, @as(f32, @floatFromInt(ctx.text_size)) / 1024, 0, 1 }; + vert.pos += ctx.position + offset; + vert.uv = .{ + vert.uv[0] * ctx.label.white_texture.width + ctx.label.white_texture.x, + vert.uv[1] * ctx.label.white_texture.height + ctx.label.white_texture.y, + }; + } + try ctx.app.vertices.appendSlice(filled_vertices_after_offset); + + if (debug_colors) { + try ctx.app.fragment_uniform_list.appendNTimes(.{ .blend_color = .{ 0, 1, 0, 1 } }, filled_vertices_after_offset.len / 3); + } else { + try ctx.app.fragment_uniform_list.appendNTimes(.{ .blend_color = ctx.text_color }, filled_vertices_after_offset.len / 3); + } + + var convex_vertices_after_offset = try ctx.label.allocator.alloc(Vertex, v.value_ptr.convex_vertices.items.len + v.value_ptr.convex_vertices_indices.items.len); + defer ctx.label.allocator.free(convex_vertices_after_offset); + var j: u16 = 0; + var k: u16 = 0; + var convex_vertices_consumed: usize = 0; + while (j < convex_vertices_after_offset.len) : (j += 3) { + convex_vertices_after_offset[j] = v.value_ptr.convex_vertices.items[j / 3]; + convex_vertices_consumed += 1; + + convex_vertices_after_offset[j].pos *= Vec4{ @as(f32, @floatFromInt(ctx.text_size)) / 1024, @as(f32, @floatFromInt(ctx.text_size)) / 1024, 0, 1 }; + convex_vertices_after_offset[j].pos += ctx.position + offset; + convex_vertices_after_offset[j].uv = .{ + convex_vertices_after_offset[j].uv[0] * ctx.label.white_texture.width + ctx.label.white_texture.x, + convex_vertices_after_offset[j].uv[1] * ctx.label.white_texture.height + ctx.label.white_texture.y, + }; + + convex_vertices_after_offset[j + 1] = filled_vertices_after_offset[v.value_ptr.convex_vertices_indices.items[k]]; + convex_vertices_after_offset[j + 2] = filled_vertices_after_offset[v.value_ptr.convex_vertices_indices.items[k + 1]]; + k += 2; + } + std.debug.assert(convex_vertices_consumed == v.value_ptr.convex_vertices.items.len); + try ctx.app.vertices.appendSlice(convex_vertices_after_offset); + + if (debug_colors) { + try ctx.app.fragment_uniform_list.appendNTimes(.{ .type = .quadratic_convex, .blend_color = .{ 1, 0, 0, 1 } }, convex_vertices_after_offset.len / 3); + } else { + try ctx.app.fragment_uniform_list.appendNTimes(.{ .type = .quadratic_convex, .blend_color = ctx.text_color }, convex_vertices_after_offset.len / 3); + } + + const concave_vertices_after_offset = try ctx.label.allocator.alloc(Vertex, v.value_ptr.concave_vertices.items.len); + defer ctx.label.allocator.free(concave_vertices_after_offset); + for (concave_vertices_after_offset, 0..) |*vert, i| { + vert.* = filled_vertices_after_offset[v.value_ptr.concave_vertices.items[i]]; + } + try ctx.app.vertices.appendSlice(concave_vertices_after_offset); + + if (debug_colors) { + try ctx.app.fragment_uniform_list.appendNTimes(.{ .type = .quadratic_concave, .blend_color = .{ 0, 0, 1, 1 } }, concave_vertices_after_offset.len / 3); + } else { + try ctx.app.fragment_uniform_list.appendNTimes(.{ .type = .quadratic_concave, .blend_color = ctx.text_color }, concave_vertices_after_offset.len / 3); + } + + ctx.app.update_vertex_buffer = true; + ctx.app.update_frag_uniform_buffer = true; + + offset[0] += @as(f32, @floatFromInt(ctx.label.face.glyph().metrics().horiAdvance)) * (@as(f32, @floatFromInt(ctx.text_size)) / 1024); + }, + } + } + return bytes.len; +} + +// First move to initialize the outline, (first point), +// After many Q L or C, we will come back to the first point and then call M again if we need to hollow +// On the second M, we instead use an L to connect the first point to the start of the hollow path. +// We then follow like normal and at the end of the hollow path we use another L to close the path. + +// This is basically how an o would be drawn, each ┌... character is a Vertex +// ┌--------┐ +// | | +// | | +// | | +// | ┌----┐ | +// └-┘ | | Consider the vertices here and below to be at the same height, they are coincident +// ┌-┐ | | +// | └----┘ | +// | | +// | | +// | | +// └--------┘ + +const OutlineContext = struct { + /// There may be more than one polygon (for example with 'i' we have the polygon of the base and + /// another for the circle) + outline_verts: std.ArrayList(std.ArrayList(Vec2)), + + /// The internal outline, used for carving the shape. For example in 'a', we would first get the + /// outline of the entire 'a', but if we stopped there, the center hole would be filled, so we + /// need another outline for carving the filled polygon. + inside_verts: std.ArrayList(Vec2), + + /// For the concave (inner 'o') and convex (outer 'o') beziers + concave_vertices: std.ArrayList(Vec2), + convex_vertices: std.ArrayList(Vec2), +}; + +/// If there are elements in inside_verts, unite them with the outline_verts, effectively carving +/// the shape +fn uniteOutsideAndInsideVertices(ctx: *OutlineContext) void { + if (ctx.inside_verts.items.len != 0) { + // Check which point of outline is closer to the first of inside + var last_outline = &ctx.outline_verts.items[ctx.outline_verts.items.len - 1]; + if (last_outline.items.len == 0 and ctx.outline_verts.items.len >= 2) { + last_outline = &ctx.outline_verts.items[ctx.outline_verts.items.len - 2]; + } + std.debug.assert(last_outline.items.len != 0); + const closest_to_inside: usize = blk: { + const first_point_inside = ctx.inside_verts.items[0]; + var min = math.floatMax(f32); + var closest_index: usize = undefined; + + for (last_outline.items, 0..) |item, i| { + const dist = @reduce(.Add, (item - first_point_inside) * (item - first_point_inside)); + if (dist < min) { + min = dist; + closest_index = i; + } + } + break :blk closest_index; + }; + + ctx.inside_verts.append(last_outline.items[closest_to_inside]) catch unreachable; + last_outline.insertSlice(closest_to_inside + 1, ctx.inside_verts.items) catch unreachable; + ctx.inside_verts.clearRetainingCapacity(); + } +} + +// TODO: Return also allocation error +fn moveToFunction(ctx: *OutlineContext, _to: ft.Vector) ft.Error!void { + uniteOutsideAndInsideVertices(ctx); + + const to = Vec2{ @as(f32, @floatFromInt(_to.x)), @as(f32, @floatFromInt(_to.y)) }; + + // To check wether a point is carving a polygon, use the point-in-polygon test to determine if + // we're inside or outside of the polygon. + const new_point_is_inside = pointInPolygon(to, ctx.outline_verts.items); + + if (ctx.outline_verts.items.len == 0 or ctx.outline_verts.items[ctx.outline_verts.items.len - 1].items.len > 0) { + // The last polygon we were building is now finished. + const new_outline_list = std.ArrayList(Vec2).init(ctx.outline_verts.allocator); + ctx.outline_verts.append(new_outline_list) catch unreachable; + } + + if (new_point_is_inside) { + ctx.inside_verts.append(to) catch unreachable; + } else { + ctx.outline_verts.items[ctx.outline_verts.items.len - 1].append(to) catch unreachable; + } +} + +fn lineToFunction(ctx: *OutlineContext, to: ft.Vector) ft.Error!void { + // std.log.info("L {} {}", .{ to.x, to.y }); + + // If inside_verts is not empty, we need to fill it + if (ctx.inside_verts.items.len != 0) { + ctx.inside_verts.append(.{ @as(f32, @floatFromInt(to.x)), @as(f32, @floatFromInt(to.y)) }) catch unreachable; + } else { + // Otherwise append the new point to the last polygon + ctx.outline_verts.items[ctx.outline_verts.items.len - 1].append(.{ @as(f32, @floatFromInt(to.x)), @as(f32, @floatFromInt(to.y)) }) catch unreachable; + } +} + +/// Called to indicate that a quadratic bezier curve occured between the previous point on the glyph +/// outline to the `_to` point on the path, with the specified `_control` quadratic bezier control +/// point. +fn conicToFunction(ctx: *OutlineContext, _control: ft.Vector, _to: ft.Vector) ft.Error!void { + // std.log.info("C {} {} {} {}", .{ control.x, control.y, to.x, to.y }); + const control = Vec2{ @as(f32, @floatFromInt(_control.x)), @as(f32, @floatFromInt(_control.y)) }; + const to = Vec2{ @as(f32, @floatFromInt(_to.x)), @as(f32, @floatFromInt(_to.y)) }; + + // If our last point was inside the glyph (e.g. the hole in the letter 'o') then this is a + // continuation of that path, and we should write this vertex to inside_verts. Otherwise we're + // on the outside and the vertex should go in outline_verts. + // + // We derive if we're on the inside or outside based on whether inside_verts has items in it, + // because only a lineTo callback can move us from the inside to the outside or vice-versa. A + // quadratic bezier would *always* be the continuation of an inside or outside path. + var verts_to_write = if (ctx.inside_verts.items.len != 0) &ctx.inside_verts else &ctx.outline_verts.items[ctx.outline_verts.items.len - 1]; + const previous_point = verts_to_write.items[verts_to_write.items.len - 1]; + + var vertices = [_]Vec2{ control, to, previous_point }; + + const vec1 = control - previous_point; + const vec2 = to - control; + + // CCW (convex) or CW (concave)? + if ((vec1[0] * vec2[1] - vec1[1] * vec2[0]) <= 0) { + // Convex + ctx.convex_vertices.appendSlice(&vertices) catch unreachable; + verts_to_write.append(to) catch unreachable; + return; + } + + // Concave + // + // In this case, we need to write a vertex (for the filled triangle) to the quadratic + // control point. However, since this is the concave case the control point could be outside + // the shape itself. We need to ensure it is not, otherwise the triangle would end up filling + // space outside the shape. + // + // Diagram: https://user-images.githubusercontent.com/3173176/189944586-bc1b109a-62c4-4ef5-a605-4c6a7e4a1abd.png + // + // To fix this, we must determine if the control point intersects with any of our outline + // segments. If it does, we use that intersection point as the vertex. Otherwise, it doesn't go + // past an outline segment and we can use the control point just fine. + var intersection: ?Vec2 = null; + for (ctx.outline_verts.items) |polygon| { + var i: usize = 1; + while (i < polygon.items.len) : (i += 1) { + const v1 = polygon.items[i - 1]; + const v2 = polygon.items[i]; + if (vec2Equal(v1, previous_point) or vec2Equal(v1, control) or vec2Equal(v1, to) or vec2Equal(v2, previous_point) or vec2Equal(v2, control) or vec2Equal(v2, to)) continue; + + intersection = intersectLineSegments(v1, v2, previous_point, control); + if (intersection != null) break; + } + if (intersection != null) break; + } + + if (intersection) |intersect| { + // TODO: properly scale control/intersection point a little bit towards the previous_point, + // so our tessellator doesn't get confused about it being exactly on the path. + // + // TODO(gkurve): Moving this control point changes the bezier shape (obviously) which means + // it is no longer true to the original shape. Need to fix this with some type of negative + // border on the gkurve primitive. + vertices[0] = Vec2{ intersect[0] * 0.99, intersect[1] * 0.99 }; + } + ctx.concave_vertices.appendSlice(&vertices) catch unreachable; + verts_to_write.append(vertices[0]) catch unreachable; + verts_to_write.append(to) catch unreachable; +} + +// Doesn't seem to be used much +fn cubicToFunction(ctx: *OutlineContext, control_0: ft.Vector, control_1: ft.Vector, to: ft.Vector) ft.Error!void { + _ = ctx; + _ = control_0; + _ = control_1; + _ = to; + @panic("TODO: search how to approximate cubic bezier with quadratic ones"); +} + +pub fn print(label: *ResizableLabel, app: *App, comptime fmt: []const u8, args: anytype, position: Vec4, text_color: Vec4, text_size: u32) !void { + const w = writer(label, app, position, text_color, text_size); + try w.print(fmt, args); +} + +/// Intersects the line segments [p0, p1] and [p2, p3], returning the intersection point if any. +fn intersectLineSegments(p0: Vec2, p1: Vec2, p2: Vec2, p3: Vec2) ?Vec2 { + const s1 = Vec2{ p1[0] - p0[0], p1[1] - p0[1] }; + const s2 = Vec2{ p3[0] - p2[0], p3[1] - p2[1] }; + const s = (-s1[1] * (p0[0] - p2[0]) + s1[0] * (p0[1] - p2[1])) / (-s2[0] * s1[1] + s1[0] * s2[1]); + const t = (s2[0] * (p0[1] - p2[1]) - s2[1] * (p0[0] - p2[0])) / (-s2[0] * s1[1] + s1[0] * s2[1]); + + if (s >= 0 and s <= 1 and t >= 0 and t <= 1) { + // Collision + return Vec2{ p0[0] + (t * s1[0]), p0[1] + (t * s1[1]) }; + } + return null; // No collision +} + +fn intersectRayToLineSegment(ray_origin: Vec2, ray_direction: Vec2, p1: Vec2, p2: Vec2) ?Vec2 { + return intersectLineSegments(ray_origin, ray_origin * (ray_direction * Vec2{ 10000000.0, 10000000.0 }), p1, p2); +} + +fn vec2Equal(a: Vec2, b: Vec2) bool { + return a[0] == b[0] and a[1] == b[1]; +} + +fn vec2CrossProduct(a: Vec2, b: Vec2) f32 { + return (a[0] * b[1]) - (a[1] * b[0]); +} + +fn pointInPolygon(p: Vec2, polygon: []std.ArrayList(Vec2)) bool { + // Cast a ray to the right of the point and check + // when this ray intersects the edges of the polygons, + // if the number of intersections is odd -> inside, + // if it's even -> outside + var is_inside = false; + for (polygon) |contour| { + var i: usize = 1; + while (i < contour.items.len) : (i += 1) { + const v1 = contour.items[i - 1]; + const v2 = contour.items[i]; + + if (intersectRayToLineSegment(p, Vec2{ 1, p[1] }, v1, v2)) |_| { + is_inside = !is_inside; + } + } + } + return is_inside; +} diff --git a/examples/gkurve/tracy.zig b/examples/gkurve/tracy.zig new file mode 100644 index 00000000..00ff9fa8 --- /dev/null +++ b/examples/gkurve/tracy.zig @@ -0,0 +1,314 @@ +// Copied from zig/src/tracy.zig + +const std = @import("std"); +const builtin = @import("builtin"); +// TODO: integrate with tracy? +// const build_options = @import("build_options"); + +// pub const enable = if (builtin.is_test) false else build_options.enable_tracy; +// pub const enable_allocation = enable and build_options.enable_tracy_allocation; +// pub const enable_callstack = enable and build_options.enable_tracy_callstack; +pub const enable = false; +pub const enable_allocation = enable and false; +pub const enable_callstack = enable and false; + +// TODO: make this configurable +const callstack_depth = 10; + +const ___tracy_c_zone_context = extern struct { + id: u32, + active: c_int, + + pub inline fn end(self: @This()) void { + ___tracy_emit_zone_end(self); + } + + pub inline fn addText(self: @This(), text: []const u8) void { + ___tracy_emit_zone_text(self, text.ptr, text.len); + } + + pub inline fn setName(self: @This(), name: []const u8) void { + ___tracy_emit_zone_name(self, name.ptr, name.len); + } + + pub inline fn setColor(self: @This(), color: u32) void { + ___tracy_emit_zone_color(self, color); + } + + pub inline fn setValue(self: @This(), value: u64) void { + ___tracy_emit_zone_value(self, value); + } +}; + +pub const Ctx = if (enable) ___tracy_c_zone_context else struct { + pub inline fn end(self: @This()) void { + _ = self; + } + + pub inline fn addText(self: @This(), text: []const u8) void { + _ = self; + _ = text; + } + + pub inline fn setName(self: @This(), name: []const u8) void { + _ = self; + _ = name; + } + + pub inline fn setColor(self: @This(), color: u32) void { + _ = self; + _ = color; + } + + pub inline fn setValue(self: @This(), value: u64) void { + _ = self; + _ = value; + } +}; + +pub inline fn trace(comptime src: std.builtin.SourceLocation) Ctx { + if (!enable) return .{}; + + if (enable_callstack) { + return ___tracy_emit_zone_begin_callstack(&.{ + .name = null, + .function = src.fn_name.ptr, + .file = src.file.ptr, + .line = src.line, + .color = 0, + }, callstack_depth, 1); + } else { + return ___tracy_emit_zone_begin(&.{ + .name = null, + .function = src.fn_name.ptr, + .file = src.file.ptr, + .line = src.line, + .color = 0, + }, 1); + } +} + +pub inline fn traceNamed(comptime src: std.builtin.SourceLocation, comptime name: [:0]const u8) Ctx { + if (!enable) return .{}; + + if (enable_callstack) { + return ___tracy_emit_zone_begin_callstack(&.{ + .name = name.ptr, + .function = src.fn_name.ptr, + .file = src.file.ptr, + .line = src.line, + .color = 0, + }, callstack_depth, 1); + } else { + return ___tracy_emit_zone_begin(&.{ + .name = name.ptr, + .function = src.fn_name.ptr, + .file = src.file.ptr, + .line = src.line, + .color = 0, + }, 1); + } +} + +pub fn tracyAllocator(allocator: std.mem.Allocator) TracyAllocator(null) { + return TracyAllocator(null).init(allocator); +} + +pub fn TracyAllocator(comptime name: ?[:0]const u8) type { + return struct { + parent_allocator: std.mem.Allocator, + + const Self = @This(); + + pub fn init(parent_allocator: std.mem.Allocator) Self { + return .{ + .parent_allocator = parent_allocator, + }; + } + + pub fn allocator(self: *Self) std.mem.Allocator { + return std.mem.Allocator.init(self, allocFn, resizeFn, freeFn); + } + + fn allocFn(self: *Self, len: usize, ptr_align: u29, len_align: u29, ret_addr: usize) std.mem.Allocator.Error![]u8 { + const result = self.parent_allocator.rawAlloc(len, ptr_align, len_align, ret_addr); + if (result) |data| { + if (data.len != 0) { + if (name) |n| { + allocNamed(data.ptr, data.len, n); + } else { + alloc(data.ptr, data.len); + } + } + } else |_| { + messageColor("allocation failed", 0xFF0000); + } + return result; + } + + fn resizeFn(self: *Self, buf: []u8, buf_align: u29, new_len: usize, len_align: u29, ret_addr: usize) ?usize { + if (self.parent_allocator.rawResize(buf, buf_align, new_len, len_align, ret_addr)) |resized_len| { + if (name) |n| { + freeNamed(buf.ptr, n); + allocNamed(buf.ptr, resized_len, n); + } else { + free(buf.ptr); + alloc(buf.ptr, resized_len); + } + + return resized_len; + } + + // during normal operation the compiler hits this case thousands of times due to this + // emitting messages for it is both slow and causes clutter + return null; + } + + fn freeFn(self: *Self, buf: []u8, buf_align: u29, ret_addr: usize) void { + self.parent_allocator.rawFree(buf, buf_align, ret_addr); + // this condition is to handle free being called on an empty slice that was never even allocated + // example case: `std.process.getSelfExeSharedLibPaths` can return `&[_][:0]u8{}` + if (buf.len != 0) { + if (name) |n| { + freeNamed(buf.ptr, n); + } else { + free(buf.ptr); + } + } + } + }; +} + +// This function only accepts comptime known strings, see `messageCopy` for runtime strings +pub inline fn message(comptime msg: [:0]const u8) void { + if (!enable) return; + ___tracy_emit_messageL(msg.ptr, if (enable_callstack) callstack_depth else 0); +} + +// This function only accepts comptime known strings, see `messageColorCopy` for runtime strings +pub inline fn messageColor(comptime msg: [:0]const u8, color: u32) void { + if (!enable) return; + ___tracy_emit_messageLC(msg.ptr, color, if (enable_callstack) callstack_depth else 0); +} + +pub inline fn messageCopy(msg: []const u8) void { + if (!enable) return; + ___tracy_emit_message(msg.ptr, msg.len, if (enable_callstack) callstack_depth else 0); +} + +pub inline fn messageColorCopy(msg: [:0]const u8, color: u32) void { + if (!enable) return; + ___tracy_emit_messageC(msg.ptr, msg.len, color, if (enable_callstack) callstack_depth else 0); +} + +pub inline fn frameMark() void { + if (!enable) return; + ___tracy_emit_frame_mark(null); +} + +pub inline fn frameMarkNamed(comptime name: [:0]const u8) void { + if (!enable) return; + ___tracy_emit_frame_mark(name.ptr); +} + +pub inline fn namedFrame(comptime name: [:0]const u8) Frame(name) { + frameMarkStart(name); + return .{}; +} + +pub fn Frame(comptime name: [:0]const u8) type { + return struct { + pub fn end(_: @This()) void { + frameMarkEnd(name); + } + }; +} + +inline fn frameMarkStart(comptime name: [:0]const u8) void { + if (!enable) return; + ___tracy_emit_frame_mark_start(name.ptr); +} + +inline fn frameMarkEnd(comptime name: [:0]const u8) void { + if (!enable) return; + ___tracy_emit_frame_mark_end(name.ptr); +} + +extern fn ___tracy_emit_frame_mark_start(name: [*:0]const u8) void; +extern fn ___tracy_emit_frame_mark_end(name: [*:0]const u8) void; + +inline fn alloc(ptr: [*]u8, len: usize) void { + if (!enable) return; + + if (enable_callstack) { + ___tracy_emit_memory_alloc_callstack(ptr, len, callstack_depth, 0); + } else { + ___tracy_emit_memory_alloc(ptr, len, 0); + } +} + +inline fn allocNamed(ptr: [*]u8, len: usize, comptime name: [:0]const u8) void { + if (!enable) return; + + if (enable_callstack) { + ___tracy_emit_memory_alloc_callstack_named(ptr, len, callstack_depth, 0, name.ptr); + } else { + ___tracy_emit_memory_alloc_named(ptr, len, 0, name.ptr); + } +} + +inline fn free(ptr: [*]u8) void { + if (!enable) return; + + if (enable_callstack) { + ___tracy_emit_memory_free_callstack(ptr, callstack_depth, 0); + } else { + ___tracy_emit_memory_free(ptr, 0); + } +} + +inline fn freeNamed(ptr: [*]u8, comptime name: [:0]const u8) void { + if (!enable) return; + + if (enable_callstack) { + ___tracy_emit_memory_free_callstack_named(ptr, callstack_depth, 0, name.ptr); + } else { + ___tracy_emit_memory_free_named(ptr, 0, name.ptr); + } +} + +extern fn ___tracy_emit_zone_begin( + srcloc: *const ___tracy_source_location_data, + active: c_int, +) ___tracy_c_zone_context; +extern fn ___tracy_emit_zone_begin_callstack( + srcloc: *const ___tracy_source_location_data, + depth: c_int, + active: c_int, +) ___tracy_c_zone_context; +extern fn ___tracy_emit_zone_text(ctx: ___tracy_c_zone_context, txt: [*]const u8, size: usize) void; +extern fn ___tracy_emit_zone_name(ctx: ___tracy_c_zone_context, txt: [*]const u8, size: usize) void; +extern fn ___tracy_emit_zone_color(ctx: ___tracy_c_zone_context, color: u32) void; +extern fn ___tracy_emit_zone_value(ctx: ___tracy_c_zone_context, value: u64) void; +extern fn ___tracy_emit_zone_end(ctx: ___tracy_c_zone_context) void; +extern fn ___tracy_emit_memory_alloc(ptr: *const anyopaque, size: usize, secure: c_int) void; +extern fn ___tracy_emit_memory_alloc_callstack(ptr: *const anyopaque, size: usize, depth: c_int, secure: c_int) void; +extern fn ___tracy_emit_memory_free(ptr: *const anyopaque, secure: c_int) void; +extern fn ___tracy_emit_memory_free_callstack(ptr: *const anyopaque, depth: c_int, secure: c_int) void; +extern fn ___tracy_emit_memory_alloc_named(ptr: *const anyopaque, size: usize, secure: c_int, name: [*:0]const u8) void; +extern fn ___tracy_emit_memory_alloc_callstack_named(ptr: *const anyopaque, size: usize, depth: c_int, secure: c_int, name: [*:0]const u8) void; +extern fn ___tracy_emit_memory_free_named(ptr: *const anyopaque, secure: c_int, name: [*:0]const u8) void; +extern fn ___tracy_emit_memory_free_callstack_named(ptr: *const anyopaque, depth: c_int, secure: c_int, name: [*:0]const u8) void; +extern fn ___tracy_emit_message(txt: [*]const u8, size: usize, callstack: c_int) void; +extern fn ___tracy_emit_messageL(txt: [*:0]const u8, callstack: c_int) void; +extern fn ___tracy_emit_messageC(txt: [*]const u8, size: usize, color: u32, callstack: c_int) void; +extern fn ___tracy_emit_messageLC(txt: [*:0]const u8, color: u32, callstack: c_int) void; +extern fn ___tracy_emit_frame_mark(name: ?[*:0]const u8) void; + +const ___tracy_source_location_data = extern struct { + name: ?[*:0]const u8, + function: [*:0]const u8, + file: [*:0]const u8, + line: u32, + color: u32, +}; diff --git a/examples/gkurve/vert.wgsl b/examples/gkurve/vert.wgsl new file mode 100644 index 00000000..94d3ac91 --- /dev/null +++ b/examples/gkurve/vert.wgsl @@ -0,0 +1,40 @@ +struct VertexUniform { + matrix: mat4x4, +} +@binding(0) @group(0) var ubo: VertexUniform; + +struct VertexOut { + @builtin(position) position_clip: vec4, + @location(0) frag_uv: vec2, + @interpolate(linear) @location(1) frag_bary: vec2, + @interpolate(flat) @location(2) triangle_index: u32, +} + +@vertex fn main( + @builtin(vertex_index) vertex_index: u32, + @location(0) position: vec4, + @location(1) uv: vec2, +) -> VertexOut { + var output : VertexOut; + output.position_clip = ubo.matrix * position; + output.frag_uv = uv; + + // Generates [0.0, 0.0], [0.5, 0.0], [1.0, 1.0] + // + // Equal to: + // + // if ((vertex_index+1u) % 3u == 0u) { + // output.frag_bary = vec2(0.0, 0.0); + // } else if ((vertex_index+1u) % 3u == 1u) { + // output.frag_bary = vec2(0.5, 0.0); + // } else { + // output.frag_bary = vec2(1.0, 1.0); + // } + // + output.frag_bary = vec2( + f32((vertex_index+1u) % 3u) * 0.5, + 1.0 - f32((((vertex_index + 3u) % 3u) + 1u) % 2u), + ); + output.triangle_index = vertex_index / 3u; + return output; +} diff --git a/examples/glyphs/Game.zig b/examples/glyphs/Game.zig new file mode 100644 index 00000000..5e119f91 --- /dev/null +++ b/examples/glyphs/Game.zig @@ -0,0 +1,211 @@ +const std = @import("std"); +const mach = @import("mach"); +const core = mach.core; +const gpu = mach.gpu; +const ecs = mach.ecs; +const Sprite = mach.gfx.Sprite; +const math = mach.math; +const vec2 = math.vec2; +const vec3 = math.vec3; +const Vec2 = math.Vec2; +const Vec3 = math.Vec3; +const Mat3x3 = math.Mat3x3; +const Mat4x4 = math.Mat4x4; + +const Text = @import("Text.zig"); + +timer: mach.Timer, +player: mach.ecs.EntityID, +direction: Vec2 = vec2(0, 0), +spawning: bool = false, +spawn_timer: mach.Timer, +fps_timer: mach.Timer, +frame_count: usize, +sprites: usize, +rand: std.rand.DefaultPrng, +time: f32, + +const d0 = 0.000001; + +// Each module must have a globally unique name declared, it is impossible to use two modules with +// the same name in a program. To avoid name conflicts, we follow naming conventions: +// +// 1. `.mach` and the `.mach_foobar` namespace is reserved for Mach itself and the modules it +// provides. +// 2. Single-word names like `.game` are reserved for the application itself. +// 3. Libraries which provide modules MUST be prefixed with an "owner" name, e.g. `.ziglibs_imgui` +// instead of `.imgui`. We encourage using e.g. your GitHub name, as these must be globally +// unique. +// +pub const name = .game; +pub const Mod = mach.Mod(@This()); + +pub const Pipeline = enum(u32) { + default, + text, +}; + +pub fn init( + engine: *mach.Engine.Mod, + sprite_mod: *Sprite.Mod, + text_mod: *Text.Mod, + game: *Mod, +) !void { + // The Mach .core is where we set window options, etc. + core.setTitle("gfx.Sprite example"); + + // Initialize mach.gfx.Text module + try text_mod.send(.init, .{}); + + // Tell sprite_mod to use the texture + try sprite_mod.send(.init, .{}); + try sprite_mod.send(.initPipeline, .{Sprite.PipelineOptions{ + .pipeline = @intFromEnum(Pipeline.text), + .texture = text_mod.state.texture, + }}); + + // We can create entities, and set components on them. Note that components live in a module + // namespace, e.g. the `Sprite` module could have a 3D `.location` component with a different + // type than the `.physics2d` module's `.location` component if you desire. + + const r = text_mod.state.regions.get('?').?; + const player = try engine.newEntity(); + try sprite_mod.set(player, .transform, Mat4x4.translate(vec3(-0.02, 0, 0))); + try sprite_mod.set(player, .size, vec2(@floatFromInt(r.width), @floatFromInt(r.height))); + try sprite_mod.set(player, .uv_transform, Mat3x3.translate(vec2(@floatFromInt(r.x), @floatFromInt(r.y)))); + try sprite_mod.set(player, .pipeline, @intFromEnum(Pipeline.text)); + try sprite_mod.send(.updated, .{@intFromEnum(Pipeline.text)}); + + game.state = .{ + .timer = try mach.Timer.start(), + .spawn_timer = try mach.Timer.start(), + .player = player, + .fps_timer = try mach.Timer.start(), + .frame_count = 0, + .sprites = 0, + .rand = std.rand.DefaultPrng.init(1337), + .time = 0, + }; +} + +pub fn tick( + engine: *mach.Engine.Mod, + sprite_mod: *Sprite.Mod, + text_mod: *Text.Mod, + game: *Mod, +) !void { + // TODO(engine): event polling should occur in mach.Engine module and get fired as ECS events. + var iter = core.pollEvents(); + var direction = game.state.direction; + var spawning = game.state.spawning; + while (iter.next()) |event| { + switch (event) { + .key_press => |ev| { + switch (ev.key) { + .left => direction.v[0] -= 1, + .right => direction.v[0] += 1, + .up => direction.v[1] += 1, + .down => direction.v[1] -= 1, + .space => spawning = true, + else => {}, + } + }, + .key_release => |ev| { + switch (ev.key) { + .left => direction.v[0] += 1, + .right => direction.v[0] -= 1, + .up => direction.v[1] -= 1, + .down => direction.v[1] += 1, + .space => spawning = false, + else => {}, + } + }, + .close => try engine.send(.exit, .{}), + else => {}, + } + } + game.state.direction = direction; + game.state.spawning = spawning; + + var player_transform = sprite_mod.get(game.state.player, .transform).?; + var player_pos = player_transform.translation(); + if (!spawning and game.state.spawn_timer.read() > 1.0 / 60.0) { + // Spawn new entities + _ = game.state.spawn_timer.lap(); + for (0..50) |_| { + var new_pos = player_pos; + new_pos.v[0] += game.state.rand.random().floatNorm(f32) * 25; + new_pos.v[1] += game.state.rand.random().floatNorm(f32) * 25; + + const rand_index = game.state.rand.random().intRangeAtMost(usize, 0, text_mod.state.regions.count() - 1); + const r = text_mod.state.regions.entries.get(rand_index).value; + + const new_entity = try engine.newEntity(); + try sprite_mod.set(new_entity, .transform, Mat4x4.translate(new_pos).mul(&Mat4x4.scaleScalar(0.3))); + try sprite_mod.set(new_entity, .size, vec2(@floatFromInt(r.width), @floatFromInt(r.height))); + try sprite_mod.set(new_entity, .uv_transform, Mat3x3.translate(vec2(@floatFromInt(r.x), @floatFromInt(r.y)))); + try sprite_mod.set(new_entity, .pipeline, @intFromEnum(Pipeline.text)); + game.state.sprites += 1; + } + } + + // Multiply by delta_time to ensure that movement is the same speed regardless of the frame rate. + const delta_time = game.state.timer.lap(); + + // Animate entities + var archetypes_iter = engine.entities.query(.{ .all = &.{ + .{ .mach_gfx_sprite = &.{.transform} }, + } }); + while (archetypes_iter.next()) |archetype| { + const ids = archetype.slice(.entity, .id); + const transforms = archetype.slice(.mach_gfx_sprite, .transform); + for (ids, transforms) |id, *old_transform| { + var location = old_transform.translation(); + if (location.x() < -@as(f32, @floatFromInt(core.size().width)) / 1.5 or location.x() > @as(f32, @floatFromInt(core.size().width)) / 1.5 or location.y() < -@as(f32, @floatFromInt(core.size().height)) / 1.5 or location.y() > @as(f32, @floatFromInt(core.size().height)) / 1.5) { + try engine.entities.remove(id); + game.state.sprites -= 1; + continue; + } + + var transform = Mat4x4.ident; + transform = transform.mul(&Mat4x4.scale(Vec3.splat(1.0 + (0.2 * delta_time)))); + transform = transform.mul(&Mat4x4.translate(location)); + transform = transform.mul(&Mat4x4.rotateZ(2 * math.pi * game.state.time)); + transform = transform.mul(&Mat4x4.scale(Vec3.splat(@max(math.cos(game.state.time / 2.0), 0.2)))); + + // TODO: .set() API is substantially slower due to internals + // try sprite_mod.set(id, .transform, transform); + old_transform.* = transform; + } + } + + // Calculate the player position, by moving in the direction the player wants to go + // by the speed amount. + const speed = 200.0; + player_pos.v[0] += direction.x() * speed * delta_time; + player_pos.v[1] += direction.y() * speed * delta_time; + player_transform = Mat4x4.translate(player_pos).mul( + &Mat4x4.scale(Vec3.splat(1.0)), + ); + try sprite_mod.set(game.state.player, .transform, player_transform); + + try sprite_mod.send(.updated, .{@intFromEnum(Pipeline.text)}); + + // Perform pre-render work + try sprite_mod.send(.preRender, .{@intFromEnum(Pipeline.text)}); + + // Render a frame + try engine.send(.beginPass, .{gpu.Color{ .r = 1.0, .g = 1.0, .b = 1.0, .a = 1.0 }}); + try sprite_mod.send(.render, .{@intFromEnum(Pipeline.text)}); + try engine.send(.endPass, .{}); + try engine.send(.present, .{}); // Present the frame + + // Every second, update the window title with the FPS + if (game.state.fps_timer.read() >= 1.0) { + try core.printTitle("gfx.Sprite example [ FPS: {d} ] [ Sprites: {d} ]", .{ game.state.frame_count, game.state.sprites }); + game.state.fps_timer.reset(); + game.state.frame_count = 0; + } + game.state.frame_count += 1; + game.state.time += delta_time; +} diff --git a/examples/glyphs/Text.zig b/examples/glyphs/Text.zig new file mode 100644 index 00000000..50d32808 --- /dev/null +++ b/examples/glyphs/Text.zig @@ -0,0 +1,123 @@ +const mach = @import("mach"); +const gpu = mach.gpu; +const ecs = mach.ecs; +const ft = @import("freetype"); +const std = @import("std"); +const assets = @import("assets"); + +pub const name = .game_text; +pub const Mod = mach.Mod(@This()); + +const RegionMap = std.AutoArrayHashMapUnmanaged(u21, mach.gfx.Atlas.Region); + +texture_atlas: mach.gfx.Atlas, +texture: *gpu.Texture, +ft: ft.Library, +face: ft.Face, +regions: RegionMap = .{}, + +pub fn deinit( + engine: *mach.Engine.Mod, + text_mod: *Mod, +) !void { + text_mod.state.texture_atlas.deinit(engine.allocator); + text_mod.state.texture.release(); + text_mod.state.face.deinit(); + text_mod.state.ft.deinit(); + text_mod.state.regions.deinit(engine.allocator); +} + +pub const local = struct { + pub fn init( + engine: *mach.Engine.Mod, + text_mod: *Mod, + ) !void { + const device = engine.state.device; + + // rgba32_pixels + const img_size = gpu.Extent3D{ .width = 1024, .height = 1024 }; + + // Create a GPU texture + const texture = device.createTexture(&.{ + .size = img_size, + .format = .rgba8_unorm, + .usage = .{ + .texture_binding = true, + .copy_dst = true, + .render_attachment = true, + }, + }); + + var s = &text_mod.state; + s.texture = texture; + s.texture_atlas = try mach.gfx.Atlas.init( + engine.allocator, + img_size.width, + .rgba, + ); + + // TODO: state fields' default values do not work + s.regions = .{}; + + s.ft = try ft.Library.init(); + s.face = try s.ft.createFaceMemory(assets.roboto_medium_ttf, 0); + + try text_mod.send(.prepare, .{&[_]u21{ '?', '!', 'a', 'b', '#', '@', '%', '$', '&', '^', '*', '+', '=', '<', '>', '/', ':', ';', 'Q', '~' }}); + } + + pub fn prepare( + engine: *mach.Engine.Mod, + text_mod: *Mod, + codepoints: []const u21, + ) !void { + const device = engine.state.device; + const queue = device.getQueue(); + var s = &text_mod.state; + + for (codepoints) |codepoint| { + const font_size = 48 * 1; + try s.face.setCharSize(font_size * 64, 0, 50, 0); + try s.face.loadChar(codepoint, .{ .render = true }); + const glyph = s.face.glyph(); + const metrics = glyph.metrics(); + + const glyph_bitmap = glyph.bitmap(); + const glyph_width = glyph_bitmap.width(); + const glyph_height = glyph_bitmap.rows(); + + // Add 1 pixel padding to texture to avoid bleeding over other textures + const margin = 1; + const glyph_data = try engine.allocator.alloc([4]u8, (glyph_width + (margin * 2)) * (glyph_height + (margin * 2))); + defer engine.allocator.free(glyph_data); + const glyph_buffer = glyph_bitmap.buffer().?; + for (glyph_data, 0..) |*data, i| { + const x = i % (glyph_width + (margin * 2)); + const y = i / (glyph_width + (margin * 2)); + if (x < margin or x > (glyph_width + margin) or y < margin or y > (glyph_height + margin)) { + data.* = [4]u8{ 0, 0, 0, 0 }; + } else { + const alpha = glyph_buffer[((y - margin) * glyph_width + (x - margin)) % glyph_buffer.len]; + data.* = [4]u8{ 0, 0, 0, alpha }; + } + } + var glyph_atlas_region = try s.texture_atlas.reserve(engine.allocator, glyph_width + (margin * 2), glyph_height + (margin * 2)); + s.texture_atlas.set(glyph_atlas_region, @as([*]const u8, @ptrCast(glyph_data.ptr))[0 .. glyph_data.len * 4]); + + glyph_atlas_region.x += margin; + glyph_atlas_region.y += margin; + glyph_atlas_region.width -= margin * 2; + glyph_atlas_region.height -= margin * 2; + + try s.regions.put(engine.allocator, codepoint, glyph_atlas_region); + _ = metrics; + } + + // rgba32_pixels + const img_size = gpu.Extent3D{ .width = 1024, .height = 1024 }; + const data_layout = gpu.Texture.DataLayout{ + .bytes_per_row = @as(u32, @intCast(img_size.width * 4)), + .rows_per_image = @as(u32, @intCast(img_size.height)), + }; + queue.writeTexture(&.{ .texture = s.texture }, &data_layout, &img_size, s.texture_atlas.data); + } +}; diff --git a/examples/glyphs/main.zig b/examples/glyphs/main.zig new file mode 100644 index 00000000..d2d7e1d3 --- /dev/null +++ b/examples/glyphs/main.zig @@ -0,0 +1,16 @@ +// Experimental ECS app example. Not yet ready for actual use. +const mach = @import("mach"); + +const Game = @import("Game.zig"); +const Text = @import("Text.zig"); + +// The list of modules to be used in our application. Our game itself is implemented in our own +// module called Game. +pub const modules = .{ + mach.Engine, + mach.gfx.Sprite, + Text, + Game, +}; + +pub const App = mach.App; diff --git a/examples/sprite/Game.zig b/examples/sprite/Game.zig new file mode 100644 index 00000000..f1f8df71 --- /dev/null +++ b/examples/sprite/Game.zig @@ -0,0 +1,241 @@ +const std = @import("std"); +const zigimg = @import("zigimg"); +const assets = @import("assets"); +const mach = @import("mach"); +const core = mach.core; +const gpu = mach.gpu; +const ecs = mach.ecs; +const Sprite = mach.gfx.Sprite; +const math = mach.math; + +const vec2 = math.vec2; +const vec3 = math.vec3; +const Vec2 = math.Vec2; +const Vec3 = math.Vec3; +const Mat3x3 = math.Mat3x3; +const Mat4x4 = math.Mat4x4; + +timer: mach.Timer, +player: mach.ecs.EntityID, +direction: Vec2 = vec2(0, 0), +spawning: bool = false, +spawn_timer: mach.Timer, +fps_timer: mach.Timer, +frame_count: usize, +sprites: usize, +rand: std.rand.DefaultPrng, +time: f32, + +const d0 = 0.000001; + +// Each module must have a globally unique name declared, it is impossible to use two modules with +// the same name in a program. To avoid name conflicts, we follow naming conventions: +// +// 1. `.mach` and the `.mach_foobar` namespace is reserved for Mach itself and the modules it +// provides. +// 2. Single-word names like `.game` are reserved for the application itself. +// 3. Libraries which provide modules MUST be prefixed with an "owner" name, e.g. `.ziglibs_imgui` +// instead of `.imgui`. We encourage using e.g. your GitHub name, as these must be globally +// unique. +// +pub const name = .game; +pub const Mod = mach.Mod(@This()); + +pub const Pipeline = enum(u32) { + default, +}; + +pub fn init( + engine: *mach.Engine.Mod, + sprite_mod: *Sprite.Mod, + game: *Mod, +) !void { + // The Mach .core is where we set window options, etc. + core.setTitle("gfx.Sprite example"); + + // We can create entities, and set components on them. Note that components live in a module + // namespace, e.g. the `.mach_gfx_sprite` module could have a 3D `.location` component with a different + // type than the `.physics2d` module's `.location` component if you desire. + + const player = try engine.newEntity(); + try sprite_mod.set(player, .transform, Mat4x4.translate(vec3(-0.02, 0, 0))); + try sprite_mod.set(player, .size, vec2(32, 32)); + try sprite_mod.set(player, .uv_transform, Mat3x3.translate(vec2(0, 0))); + try sprite_mod.set(player, .pipeline, @intFromEnum(Pipeline.default)); + + try sprite_mod.send(.init, .{}); + try sprite_mod.send(.initPipeline, .{Sprite.PipelineOptions{ + .pipeline = @intFromEnum(Pipeline.default), + .texture = try loadTexture(engine), + }}); + try sprite_mod.send(.updated, .{@intFromEnum(Pipeline.default)}); + + game.state = .{ + .timer = try mach.Timer.start(), + .spawn_timer = try mach.Timer.start(), + .player = player, + .fps_timer = try mach.Timer.start(), + .frame_count = 0, + .sprites = 0, + .rand = std.rand.DefaultPrng.init(1337), + .time = 0, + }; +} + +pub fn tick( + engine: *mach.Engine.Mod, + sprite_mod: *Sprite.Mod, + game: *Mod, +) !void { + // TODO(engine): event polling should occur in mach.Engine module and get fired as ECS events. + var iter = core.pollEvents(); + var direction = game.state.direction; + var spawning = game.state.spawning; + while (iter.next()) |event| { + switch (event) { + .key_press => |ev| { + switch (ev.key) { + .left => direction.v[0] -= 1, + .right => direction.v[0] += 1, + .up => direction.v[1] += 1, + .down => direction.v[1] -= 1, + .space => spawning = true, + else => {}, + } + }, + .key_release => |ev| { + switch (ev.key) { + .left => direction.v[0] += 1, + .right => direction.v[0] -= 1, + .up => direction.v[1] -= 1, + .down => direction.v[1] += 1, + .space => spawning = false, + else => {}, + } + }, + .close => try engine.send(.exit, .{}), + else => {}, + } + } + game.state.direction = direction; + game.state.spawning = spawning; + + var player_transform = sprite_mod.get(game.state.player, .transform).?; + var player_pos = player_transform.translation(); + if (spawning and game.state.spawn_timer.read() > 1.0 / 60.0) { + // Spawn new entities + _ = game.state.spawn_timer.lap(); + for (0..100) |_| { + var new_pos = player_pos; + new_pos.v[0] += game.state.rand.random().floatNorm(f32) * 25; + new_pos.v[1] += game.state.rand.random().floatNorm(f32) * 25; + + const new_entity = try engine.newEntity(); + try sprite_mod.set(new_entity, .transform, Mat4x4.translate(new_pos).mul(&Mat4x4.scale(Vec3.splat(0.3)))); + try sprite_mod.set(new_entity, .size, vec2(32, 32)); + try sprite_mod.set(new_entity, .uv_transform, Mat3x3.translate(vec2(0, 0))); + try sprite_mod.set(new_entity, .pipeline, @intFromEnum(Pipeline.default)); + game.state.sprites += 1; + } + } + + // Multiply by delta_time to ensure that movement is the same speed regardless of the frame rate. + const delta_time = game.state.timer.lap(); + + // Rotate entities + var archetypes_iter = engine.entities.query(.{ .all = &.{ + .{ .mach_gfx_sprite = &.{.transform} }, + } }); + while (archetypes_iter.next()) |archetype| { + const ids = archetype.slice(.entity, .id); + const transforms = archetype.slice(.mach_gfx_sprite, .transform); + for (ids, transforms) |id, *old_transform| { + _ = id; + const location = old_transform.*.translation(); + // var transform = old_transform.mul(&Mat4x4.translate(-location)); + // transform = mat.rotateZ(0.3 * delta_time).mul(&transform); + // transform = transform.mul(&Mat4x4.translate(location)); + var transform = Mat4x4.ident; + transform = transform.mul(&Mat4x4.translate(location)); + transform = transform.mul(&Mat4x4.rotateZ(2 * math.pi * game.state.time)); + transform = transform.mul(&Mat4x4.scaleScalar(@min(math.cos(game.state.time / 2.0), 0.5))); + + // TODO: .set() API is substantially slower due to internals + // try sprite_mod.set(id, .transform, transform); + old_transform.* = transform; + } + } + + // Calculate the player position, by moving in the direction the player wants to go + // by the speed amount. + const speed = 200.0; + player_pos.v[0] += direction.x() * speed * delta_time; + player_pos.v[1] += direction.y() * speed * delta_time; + try sprite_mod.set(game.state.player, .transform, Mat4x4.translate(player_pos)); + try sprite_mod.send(.updated, .{@intFromEnum(Pipeline.default)}); + + // Perform pre-render work + try sprite_mod.send(.preRender, .{@intFromEnum(Pipeline.default)}); + + // Render a frame + try engine.send(.beginPass, .{gpu.Color{ .r = 1.0, .g = 1.0, .b = 1.0, .a = 1.0 }}); + try sprite_mod.send(.render, .{@intFromEnum(Pipeline.default)}); + try engine.send(.endPass, .{}); + try engine.send(.present, .{}); // Present the frame + + // Every second, update the window title with the FPS + if (game.state.fps_timer.read() >= 1.0) { + try core.printTitle("gfx.Sprite example [ FPS: {d} ] [ Sprites: {d} ]", .{ game.state.frame_count, game.state.sprites }); + game.state.fps_timer.reset(); + game.state.frame_count = 0; + } + game.state.frame_count += 1; + game.state.time += delta_time; +} + +// TODO: move this helper into gfx module +fn loadTexture( + engine: *mach.Engine.Mod, +) !*gpu.Texture { + const device = engine.state.device; + const queue = device.getQueue(); + + // Load the image from memory + var img = try zigimg.Image.fromMemory(engine.allocator, assets.sprites_sheet_png); + defer img.deinit(); + const img_size = gpu.Extent3D{ .width = @as(u32, @intCast(img.width)), .height = @as(u32, @intCast(img.height)) }; + + // Create a GPU texture + const texture = device.createTexture(&.{ + .size = img_size, + .format = .rgba8_unorm, + .usage = .{ + .texture_binding = true, + .copy_dst = true, + .render_attachment = true, + }, + }); + const data_layout = gpu.Texture.DataLayout{ + .bytes_per_row = @as(u32, @intCast(img.width * 4)), + .rows_per_image = @as(u32, @intCast(img.height)), + }; + switch (img.pixels) { + .rgba32 => |pixels| queue.writeTexture(&.{ .texture = texture }, &data_layout, &img_size, pixels), + .rgb24 => |pixels| { + const data = try rgb24ToRgba32(engine.allocator, pixels); + defer data.deinit(engine.allocator); + queue.writeTexture(&.{ .texture = texture }, &data_layout, &img_size, data.rgba32); + }, + else => @panic("unsupported image color format"), + } + return texture; +} + +fn rgb24ToRgba32(allocator: std.mem.Allocator, in: []zigimg.color.Rgb24) !zigimg.color.PixelStorage { + const out = try zigimg.color.PixelStorage.init(allocator, .rgba32, in.len); + var i: usize = 0; + while (i < in.len) : (i += 1) { + out.rgba32[i] = zigimg.color.Rgba32{ .r = in[i].r, .g = in[i].g, .b = in[i].b, .a = 255 }; + } + return out; +} diff --git a/examples/sprite/main.zig b/examples/sprite/main.zig new file mode 100644 index 00000000..45bed864 --- /dev/null +++ b/examples/sprite/main.zig @@ -0,0 +1,14 @@ +// Experimental ECS app example. Not yet ready for actual use. +const mach = @import("mach"); + +const Game = @import("Game.zig"); + +// The list of modules to be used in our application. Our game itself is implemented in our own +// module called Game. +pub const modules = .{ + mach.Engine, + mach.gfx.Sprite, + Game, +}; + +pub const App = mach.App; diff --git a/examples/sysaudio/main.zig b/examples/sysaudio/main.zig new file mode 100644 index 00000000..09fd0941 --- /dev/null +++ b/examples/sysaudio/main.zig @@ -0,0 +1,186 @@ +// A simple tone engine. +// +// It renders 512 tones simultaneously, each with their own frequency and duration. +// +// `keyToFrequency` can be used to convert a keyboard key to a frequency, so that the +// keys asdfghj on your QWERTY keyboard will map to the notes C/D/E/F/G/A/B[4], the +// keys above qwertyu will map to C5 and the keys below zxcvbnm will map to C3. +// +// The duration is hard-coded to 1.5s. To prevent clicking, tones are faded in linearly over +// the first 1/64th duration of the tone. To provide a cool sustained effect, tones are faded +// out using 1-log10(x*10) (google it to see how it looks, it's strong for most of the duration of +// the note then fades out slowly.) +const std = @import("std"); +const builtin = @import("builtin"); + +const mach = @import("mach"); +const math = mach.math; +const sysaudio = mach.sysaudio; + +pub const App = @This(); + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +audio_ctx: sysaudio.Context, +player: sysaudio.Player, +playing: [512]Tone = std.mem.zeroes([512]Tone), + +const Tone = struct { + frequency: f32, + sample_counter: usize, + duration: usize, +}; + +pub fn init(app: *App) !void { + try mach.core.init(.{}); + + app.audio_ctx = try sysaudio.Context.init(null, gpa.allocator(), .{}); + errdefer app.audio_ctx.deinit(); + try app.audio_ctx.refresh(); + + const device = app.audio_ctx.defaultDevice(.playback) orelse return error.NoDeviceFound; + app.player = try app.audio_ctx.createPlayer(device, writeCallback, .{ .user_data = app }); + try app.player.start(); +} + +pub fn deinit(app: *App) void { + defer _ = gpa.deinit(); + defer mach.core.deinit(); + + app.player.deinit(); + app.audio_ctx.deinit(); +} + +pub fn update(app: *App) !bool { + var iter = mach.core.pollEvents(); + while (iter.next()) |event| { + switch (event) { + .key_press => |ev| { + const vol = try app.player.volume(); + switch (ev.key) { + .down => try app.player.setVolume(@max(0.0, vol - 0.1)), + .up => try app.player.setVolume(@min(1.0, vol + 0.1)), + else => {}, + } + app.fillTone(keyToFrequency(ev.key)); + }, + .close => return true, + else => {}, + } + } + + if (builtin.cpu.arch != .wasm32) { + const back_buffer_view = mach.core.swap_chain.getCurrentTextureView().?; + + mach.core.swap_chain.present(); + back_buffer_view.release(); + } + + return false; +} + +fn writeCallback(ctx: ?*anyopaque, output: []u8) void { + const app: *App = @as(*App, @ptrCast(@alignCast(ctx))); + + // const seconds_per_frame = 1.0 / @as(f32, @floatFromInt(player.sampleRate())); + const frame_size = app.player.format().frameSize(@intCast(app.player.channels().len)); + const frames = output.len / frame_size; + _ = frames; + + var frame: usize = 0; + while (frame < output.len) : (frame += frame_size) { + // Calculate the audio sample we'll play on both channels for this frame + var sample: f32 = 0; + for (&app.playing) |*tone| { + if (tone.sample_counter >= tone.duration) continue; + + tone.sample_counter += 1; + const sample_counter = @as(f32, @floatFromInt(tone.sample_counter)); + const duration = @as(f32, @floatFromInt(tone.duration)); + + // The sine wave that plays the frequency. + const gain = 0.1; + const sine_wave = math.sin(tone.frequency * 2.0 * math.pi * sample_counter / @as(f32, @floatFromInt(app.player.sampleRate()))) * gain; + + // A number ranging from 0.0 to 1.0 in the first 1/64th of the duration of the tone. + const fade_in = @min(sample_counter / (duration / 64.0), 1.0); + + // A number ranging from 1.0 to 0.0 over half the duration of the tone. + const progression = sample_counter / duration; // 0.0 (tone start) to 1.0 (tone end) + const fade_out = 1.0 - math.clamp(math.log10(progression * 10.0), 0.0, 1.0); + + // Mix this tone into the sample we'll actually play on e.g. the speakers, reducing + // sine wave intensity if we're fading in or out over the entire duration of the + // tone. + sample += sine_wave * fade_in * fade_out; + } + + // Convert our float sample to the format the audio driver is working in + sysaudio.convertTo( + f32, + // Pass two samples (assume two channel audio) + // Note that in a real application this must match app.player.channels().len + &.{ sample, sample }, + app.player.format(), + output[frame..][0..frame_size], + ); + } +} + +pub fn fillTone(app: *App, frequency: f32) void { + for (&app.playing) |*tone| { + if (tone.sample_counter >= tone.duration) { + tone.* = Tone{ + .frequency = frequency, + .sample_counter = 0, + .duration = @as(usize, @intFromFloat(1.5 * @as(f32, @floatFromInt(app.player.sampleRate())))), // play the tone for 1.5s + }; + return; + } + } +} + +pub fn keyToFrequency(key: mach.core.Key) f32 { + // The frequencies here just come from a piano frequencies chart. You can google for them. + return switch (key) { + // First row of piano keys, the highest. + .q => 523.25, // C5 + .w => 587.33, // D5 + .e => 659.26, // E5 + .r => 698.46, // F5 + .t => 783.99, // G5 + .y => 880.0, // A5 + .u => 987.77, // B5 + .i => 1046.5, // C6 + .o => 1174.7, // D6 + .p => 1318.5, // E6 + .left_bracket => 1396.9, // F6 + .right_bracket => 1568.0, // G6 + + // Second row of piano keys, the middle. + .a => 261.63, // C4 + .s => 293.67, // D4 + .d => 329.63, // E4 + .f => 349.23, // F4 + .g => 392.0, // G4 + .h => 440.0, // A4 + .j => 493.88, // B4 + .k => 523.25, // C5 + .l => 587.33, // D5 + .semicolon => 659.26, // E5 + .apostrophe => 698.46, // F5 + + // Third row of piano keys, the lowest. + .z => 130.81, // C3 + .x => 146.83, // D3 + .c => 164.81, // E3 + .v => 174.61, // F3 + .b => 196.00, // G3 + .n => 220.0, // A3 + .m => 246.94, // B3 + .comma => 261.63, // C4 + .period => 293.67, // D4 + .slash => 329.63, // E5 + else => 0.0, + }; +} diff --git a/examples/text/Game.zig b/examples/text/Game.zig new file mode 100644 index 00000000..d523748d --- /dev/null +++ b/examples/text/Game.zig @@ -0,0 +1,234 @@ +const std = @import("std"); +const zigimg = @import("zigimg"); +const assets = @import("assets"); +const mach = @import("mach"); +const core = mach.core; +const gfx = mach.gfx; +const gpu = mach.gpu; +const ecs = mach.ecs; +const Text = mach.gfx.Text; +const math = mach.math; + +const vec2 = math.vec2; +const vec3 = math.vec3; +const vec4 = math.vec4; +const Vec2 = math.Vec2; +const Vec3 = math.Vec3; +const Mat3x3 = math.Mat3x3; +const Mat4x4 = math.Mat4x4; + +timer: mach.Timer, +player: mach.ecs.EntityID, +direction: Vec2 = vec2(0, 0), +spawning: bool = false, +spawn_timer: mach.Timer, +fps_timer: mach.Timer, +frame_count: usize, +texts: usize, +rand: std.rand.DefaultPrng, +time: f32, + +const d0 = 0.000001; + +// Each module must have a globally unique name declared, it is impossible to use two modules with +// the same name in a program. To avoid name conflicts, we follow naming conventions: +// +// 1. `.mach` and the `.mach_foobar` namespace is reserved for Mach itself and the modules it +// provides. +// 2. Single-word names like `.game` are reserved for the application itself. +// 3. Libraries which provide modules MUST be prefixed with an "owner" name, e.g. `.ziglibs_imgui` +// instead of `.imgui`. We encourage using e.g. your GitHub name, as these must be globally +// unique. +// +pub const name = .game; +pub const Mod = mach.Mod(@This()); + +pub const Pipeline = enum(u32) { + default, +}; + +const upscale = 1.0; + +pub fn init( + engine: *mach.Engine.Mod, + text_mod: *Text.Mod, + game: *Mod, +) !void { + // The Mach .core is where we set window options, etc. + core.setTitle("gfx.Text example"); + + // Create some text + const player = try engine.newEntity(); + try text_mod.set(player, .pipeline, @intFromEnum(Pipeline.default)); + try text_mod.set(player, .transform, Mat4x4.scaleScalar(upscale).mul(&Mat4x4.translate(vec3(0, 0, 0)))); + const style1 = Text.Style{ + .font_name = "Roboto Medium", // TODO + .font_size = 48 * gfx.px_per_pt, // 48pt + .font_weight = gfx.font_weight_normal, + .italic = false, + .color = vec4(0.6, 1.0, 0.6, 1.0), + }; + var style2 = style1; + style2.italic = true; + var style3 = style1; + style3.font_weight = gfx.font_weight_bold; + try text_mod.set(player, .text, &.{ + .{ + .string = "Text but with spaces 😊\nand\n", + .style = &style1, + }, + .{ + .string = "italics\nand\n", + .style = &style2, + }, + .{ + .string = "bold\nand\n", + .style = &style3, + }, + }); + + try text_mod.send(.init, .{}); + try text_mod.send(.initPipeline, .{Text.PipelineOptions{ + .pipeline = @intFromEnum(Pipeline.default), + }}); + try text_mod.send(.updated, .{@intFromEnum(Pipeline.default)}); + + game.state = .{ + .timer = try mach.Timer.start(), + .spawn_timer = try mach.Timer.start(), + .player = player, + .fps_timer = try mach.Timer.start(), + .frame_count = 0, + .texts = 0, + .rand = std.rand.DefaultPrng.init(1337), + .time = 0, + }; +} + +pub fn deinit(engine: *mach.Engine.Mod) !void { + _ = engine; +} + +pub fn tick( + engine: *mach.Engine.Mod, + text_mod: *Text.Mod, + game: *Mod, +) !void { + // TODO(engine): event polling should occur in mach.Engine module and get fired as ECS events. + var iter = core.pollEvents(); + var direction = game.state.direction; + var spawning = game.state.spawning; + while (iter.next()) |event| { + switch (event) { + .key_press => |ev| { + switch (ev.key) { + .left => direction.v[0] -= 1, + .right => direction.v[0] += 1, + .up => direction.v[1] += 1, + .down => direction.v[1] -= 1, + .space => spawning = true, + else => {}, + } + }, + .key_release => |ev| { + switch (ev.key) { + .left => direction.v[0] += 1, + .right => direction.v[0] -= 1, + .up => direction.v[1] -= 1, + .down => direction.v[1] += 1, + .space => spawning = false, + else => {}, + } + }, + .close => try engine.send(.exit, .{}), + else => {}, + } + } + game.state.direction = direction; + game.state.spawning = spawning; + + var player_transform = text_mod.get(game.state.player, .transform).?; + var player_pos = player_transform.translation().divScalar(upscale); + if (spawning and game.state.spawn_timer.read() > 1.0 / 60.0) { + // Spawn new entities + _ = game.state.spawn_timer.lap(); + for (0..1) |_| { + var new_pos = player_pos; + new_pos.v[0] += game.state.rand.random().floatNorm(f32) * 25; + new_pos.v[1] += game.state.rand.random().floatNorm(f32) * 25; + + const new_entity = try engine.newEntity(); + try text_mod.set(new_entity, .pipeline, @intFromEnum(Pipeline.default)); + try text_mod.set(new_entity, .transform, Mat4x4.scaleScalar(upscale).mul(&Mat4x4.translate(new_pos))); + + const style1 = Text.Style{ + .font_name = "Roboto Medium", // TODO + .font_size = 48 * gfx.px_per_pt, // 48pt + .font_weight = gfx.font_weight_normal, + .italic = false, + .color = vec4(0.6, 1.0, 0.6, 1.0), + }; + try text_mod.set(new_entity, .text, &.{ + .{ + .string = "!$?😊", + .style = &style1, + }, + }); + + game.state.texts += 1; + } + } + + // Multiply by delta_time to ensure that movement is the same speed regardless of the frame rate. + const delta_time = game.state.timer.lap(); + + // Rotate entities + var archetypes_iter = engine.entities.query(.{ .all = &.{ + .{ .mach_gfx_text = &.{.transform} }, + } }); + while (archetypes_iter.next()) |archetype| { + const ids = archetype.slice(.entity, .id); + const transforms = archetype.slice(.mach_gfx_text, .transform); + for (ids, transforms) |id, *old_transform| { + _ = id; + const location = old_transform.*.translation(); + // var transform = old_transform.mul(&Mat4x4.translate(-location)); + // transform = mat.rotateZ(0.3 * delta_time).mul(&transform); + // transform = transform.mul(&Mat4x4.translate(location)); + var transform = Mat4x4.ident; + transform = transform.mul(&Mat4x4.translate(location)); + transform = transform.mul(&Mat4x4.rotateZ(2 * math.pi * game.state.time)); + transform = transform.mul(&Mat4x4.scaleScalar(@min(math.cos(game.state.time / 2.0), 0.5))); + + // TODO: .set() API is substantially slower due to internals + // try text_mod.set(id, .transform, transform); + old_transform.* = transform; + } + } + + // Calculate the player position, by moving in the direction the player wants to go + // by the speed amount. + const speed = 200.0 / upscale; + player_pos.v[0] += direction.x() * speed * delta_time; + player_pos.v[1] += direction.y() * speed * delta_time; + try text_mod.set(game.state.player, .transform, Mat4x4.scaleScalar(upscale).mul(&Mat4x4.translate(player_pos))); + try text_mod.send(.updated, .{@intFromEnum(Pipeline.default)}); + + // Perform pre-render work + try text_mod.send(.preRender, .{@intFromEnum(Pipeline.default)}); + + // Render a frame + try engine.send(.beginPass, .{gpu.Color{ .r = 1.0, .g = 1.0, .b = 1.0, .a = 1.0 }}); + try text_mod.send(.render, .{@intFromEnum(Pipeline.default)}); + try engine.send(.endPass, .{}); + try engine.send(.present, .{}); // Present the frame + + // Every second, update the window title with the FPS + if (game.state.fps_timer.read() >= 1.0) { + try core.printTitle("gfx.Text example [ FPS: {d} ] [ Texts: {d} ]", .{ game.state.frame_count, game.state.texts }); + game.state.fps_timer.reset(); + game.state.frame_count = 0; + } + game.state.frame_count += 1; + game.state.time += delta_time; +} diff --git a/examples/text/main.zig b/examples/text/main.zig new file mode 100644 index 00000000..94f6511c --- /dev/null +++ b/examples/text/main.zig @@ -0,0 +1,14 @@ +// Experimental ECS app example. Not yet ready for actual use. +const mach = @import("mach"); + +const Game = @import("Game.zig"); + +// The list of modules to be used in our application. Our game itself is implemented in our own +// module called Game. +pub const modules = .{ + mach.Engine, + mach.gfx.Text, + Game, +}; + +pub const App = mach.App;