{gfx,examples}: update all to new mach.Core module API

Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
This commit is contained in:
Stephen Gutekanst 2024-04-17 11:27:41 -07:00
parent ac4fe65eb2
commit d045b34f70
13 changed files with 910 additions and 689 deletions

View file

@ -663,11 +663,10 @@ fn buildExamples(
deps: []const Dependency = &.{},
std_platform_only: bool = false,
has_assets: bool = false,
use_module_api: bool = false,
}{
.{ .name = "sysaudio", .deps = &.{}, .use_module_api = true },
.{ .name = "core-custom-entrypoint", .deps = &.{}, .use_module_api = true },
.{ .name = "custom-renderer", .deps = &.{}, .use_module_api = true },
.{ .name = "sysaudio", .deps = &.{} },
.{ .name = "core-custom-entrypoint", .deps = &.{} },
.{ .name = "custom-renderer", .deps = &.{} },
.{
.name = "sprite",
.deps = &.{ .zigimg, .assets },
@ -691,7 +690,6 @@ fn buildExamples(
if (target.result.cpu.arch == .wasm32)
break;
if (example.use_module_api) {
const exe = b.addExecutable(.{
.name = example.name,
.root_source_file = .{ .path = "examples/" ++ example.name ++ "/main.zig" },
@ -703,6 +701,11 @@ fn buildExamples(
link(b, exe, &exe.root_module);
b.installArtifact(exe);
for (example.deps) |d| {
const dep = d.dependency(b, target, optimize);
exe.root_module.addImport(dep.name, dep.module);
}
const compile_step = b.step(example.name, "Compile " ++ example.name);
compile_step.dependOn(b.getInstallStep());
@ -712,40 +715,6 @@ fn buildExamples(
const run_step = b.step("run-" ++ example.name, "Run " ++ example.name);
run_step.dependOn(&run_cmd.step);
} else {
var deps = std.ArrayList(std.Build.Module.Import).init(b.allocator);
for (example.deps) |d| try deps.append(d.dependency(b, target, optimize));
const app = try App.init(
b,
.{
.name = example.name,
.src = "examples/" ++ example.name ++ "/main.zig",
.target = target,
.optimize = optimize,
.deps = deps.items,
.res_dirs = if (example.has_assets) &.{example.name ++ "/assets"} else null,
.watch_paths = &.{"examples/" ++ example.name},
.mach_builder = b,
.mach_mod = mach_mod,
},
);
try app.link();
for (example.deps) |dep| switch (dep) {
.model3d => app.compile.linkLibrary(b.dependency("mach_model3d", .{
.target = target,
.optimize = optimize,
}).artifact("mach-model3d")),
else => {},
};
const compile_step = b.step(example.name, "Compile " ++ example.name);
compile_step.dependOn(&app.install.step);
const run_step = b.step("run-" ++ example.name, "Run " ++ example.name);
run_step.dependOn(&app.run.step);
}
}
}

View file

@ -1,7 +1,6 @@
// TODO(important): review all code in this file in-depth
const std = @import("std");
const mach = @import("mach");
const core = mach.core;
const gpu = mach.gpu;
const gfx = mach.gfx;
const math = mach.math;
@ -25,19 +24,11 @@ sprites: usize,
rand: std.rand.DefaultPrng,
time: f32,
pipeline: mach.EntityID,
frame_encoder: *gpu.CommandEncoder = undefined,
frame_render_pass: *gpu.RenderPassEncoder = undefined,
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.
//
// Define the globally unique name of our module. You can use any name here, but keep in mind no
// two modules in the program can have the same name.
pub const name = .game;
pub const Mod = mach.Mod(@This());
@ -48,6 +39,7 @@ pub const global_events = .{
pub const local_events = .{
.after_init = .{ .handler = afterInit },
.end_frame = .{ .handler = endFrame },
};
fn init(
@ -69,7 +61,7 @@ fn afterInit(
game: *Mod,
) !void {
// The Mach .core is where we set window options, etc.
core.setTitle("gfx.Sprite example");
mach.core.setTitle("gfx.Sprite example");
// Create a sprite rendering pipeline
const texture = glyphs.state().texture;
@ -103,14 +95,14 @@ fn afterInit(
}
fn tick(
engine: *mach.Engine.Mod,
core: *mach.Core.Mod,
sprite: *gfx.Sprite.Mod,
sprite_pipeline: *gfx.SpritePipeline.Mod,
glyphs: *Glyphs.Mod,
game: *Mod,
) !void {
// TODO(engine): event polling should occur in mach.Engine module and get fired as ECS events.
var iter = core.pollEvents();
// TODO(important): event polling should occur in mach.Core module and get fired as ECS events.
var iter = mach.core.pollEvents();
var direction = game.state().direction;
var spawning = game.state().spawning;
while (iter.next()) |event| {
@ -135,7 +127,7 @@ fn tick(
else => {},
}
},
.close => engine.send(.exit, .{}),
.close => core.send(.exit, .{}),
else => {},
}
}
@ -155,7 +147,7 @@ fn tick(
const rand_index = game.state().rand.random().intRangeAtMost(usize, 0, glyphs.state().regions.count() - 1);
const r = glyphs.state().regions.entries.get(rand_index).value;
const new_entity = try engine.newEntity();
const new_entity = try core.newEntity();
try sprite.set(new_entity, .transform, Mat4x4.translate(new_pos).mul(&Mat4x4.scaleScalar(0.3)));
try sprite.set(new_entity, .size, vec2(@floatFromInt(r.width), @floatFromInt(r.height)));
try sprite.set(new_entity, .uv_transform, Mat3x3.translate(vec2(@floatFromInt(r.x), @floatFromInt(r.y))));
@ -168,7 +160,7 @@ fn tick(
const delta_time = game.state().timer.lap();
// Animate entities
var archetypes_iter = engine.entities.query(.{ .all = &.{
var archetypes_iter = core.entities.query(.{ .all = &.{
.{ .mach_gfx_sprite = &.{.transform} },
} });
while (archetypes_iter.next()) |archetype| {
@ -176,8 +168,9 @@ fn tick(
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);
// TODO: formatting
if (location.x() < -@as(f32, @floatFromInt(mach.core.size().width)) / 1.5 or location.x() > @as(f32, @floatFromInt(mach.core.size().width)) / 1.5 or location.y() < -@as(f32, @floatFromInt(mach.core.size().height)) / 1.5 or location.y() > @as(f32, @floatFromInt(mach.core.size().height)) / 1.5) {
try core.entities.remove(id);
game.state().sprites -= 1;
continue;
}
@ -208,18 +201,53 @@ fn tick(
// Perform pre-render work
sprite_pipeline.send(.pre_render, .{});
// Render a frame
engine.send(.begin_pass, .{gpu.Color{ .r = 1.0, .g = 1.0, .b = 1.0, .a = 1.0 }});
// Create a command encoder for this frame
game.state().frame_encoder = mach.core.device.createCommandEncoder(null);
// Grab the back buffer of the swapchain
const back_buffer_view = mach.core.swap_chain.getCurrentTextureView().?;
defer back_buffer_view.release();
// Begin render pass
const sky_blue = gpu.Color{ .r = 0.776, .g = 0.988, .b = 1, .a = 1 };
const color_attachments = [_]gpu.RenderPassColorAttachment{.{
.view = back_buffer_view,
.clear_value = sky_blue,
.load_op = .clear,
.store_op = .store,
}};
game.state().frame_encoder = mach.core.device.createCommandEncoder(null);
game.state().frame_render_pass = game.state().frame_encoder.beginRenderPass(&gpu.RenderPassDescriptor.init(.{
.label = "main render pass",
.color_attachments = &color_attachments,
}));
// Render our sprite batch
sprite_pipeline.state().render_pass = game.state().frame_render_pass;
sprite_pipeline.send(.render, .{});
engine.send(.end_pass, .{});
engine.send(.frame_done, .{}); // Present the frame
// Finish the frame once rendering is done.
game.send(.end_frame, .{});
game.state().time += delta_time;
}
fn endFrame(game: *Mod) !void {
// Finish render pass
game.state().frame_render_pass.end();
var command = game.state().frame_encoder.finish(null);
game.state().frame_encoder.release();
defer command.release();
mach.core.queue.submit(&[_]*gpu.CommandBuffer{command});
// Present the frame
mach.core.swap_chain.present();
// 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 });
try mach.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;
}

View file

@ -38,10 +38,10 @@ fn deinit(glyphs: *Mod) !void {
}
fn init(
engine: *mach.Engine.Mod,
core: *mach.Core.Mod,
glyphs: *Mod,
) !void {
const device = engine.state().device;
const device = core.state().device;
const allocator = gpa.allocator();
// rgba32_pixels
@ -77,11 +77,11 @@ fn init(
}
fn prepare(
engine: *mach.Engine.Mod,
core: *mach.Core.Mod,
glyphs: *Mod,
codepoints: []const u21,
) !void {
const device = engine.state().device;
const device = core.state().device;
const queue = device.getQueue();
var s = glyphs.state();

View file

@ -1,16 +1,19 @@
// TODO(important): review all code in this file in-depth
// Experimental ECS app example. Not yet ready for actual use.
const mach = @import("mach");
// The list of modules to be used in our application. Our game itself is implemented in our own
// module called Game.
// The global list of Mach modules registered for use in our application.
pub const modules = .{
mach.Engine,
mach.Core,
mach.gfx.Sprite,
mach.gfx.SpritePipeline,
@import("Glyphs.zig"),
@import("Game.zig"),
};
pub const App = mach.App;
// TODO(important): use standard entrypoint instead
pub fn main() !void {
// Initialize mach.Core
try mach.core.initModule();
// Main loop
while (try mach.core.tick()) {}
}

View file

@ -3,7 +3,6 @@ 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 gfx = mach.gfx;
const math = mach.math;
@ -29,19 +28,11 @@ rand: std.rand.DefaultPrng,
time: f32,
allocator: std.mem.Allocator,
pipeline: mach.EntityID,
frame_encoder: *gpu.CommandEncoder = undefined,
frame_render_pass: *gpu.RenderPassEncoder = undefined,
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.
//
// Define the globally unique name of our module. You can use any name here, but keep in mind no
// two modules in the program can have the same name.
pub const name = .game;
pub const Mod = mach.Mod(@This());
@ -50,14 +41,18 @@ pub const global_events = .{
.tick = .{ .handler = tick },
};
pub const local_events = .{
.end_frame = .{ .handler = endFrame },
};
fn init(
engine: *mach.Engine.Mod,
core: *mach.Core.Mod,
sprite: *gfx.Sprite.Mod,
sprite_pipeline: *gfx.SpritePipeline.Mod,
game: *Mod,
) !void {
// The Mach .core is where we set window options, etc.
core.setTitle("gfx.Sprite example");
mach.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
@ -65,12 +60,12 @@ fn init(
// Create a sprite rendering pipeline
const allocator = gpa.allocator();
const pipeline = try engine.newEntity();
try sprite_pipeline.set(pipeline, .texture, try loadTexture(engine, allocator));
const pipeline = try core.newEntity();
try sprite_pipeline.set(pipeline, .texture, try loadTexture(core, allocator));
sprite_pipeline.send(.update, .{});
// Create our player sprite
const player = try engine.newEntity();
const player = try core.newEntity();
try sprite.set(player, .transform, Mat4x4.translate(vec3(-0.02, 0, 0)));
try sprite.set(player, .size, vec2(32, 32));
try sprite.set(player, .uv_transform, Mat3x3.translate(vec2(0, 0)));
@ -92,13 +87,13 @@ fn init(
}
fn tick(
engine: *mach.Engine.Mod,
core: *mach.Core.Mod,
sprite: *gfx.Sprite.Mod,
sprite_pipeline: *gfx.SpritePipeline.Mod,
game: *Mod,
) !void {
// TODO(engine): event polling should occur in mach.Engine module and get fired as ECS events.
var iter = core.pollEvents();
// TODO(important): event polling should occur in mach.Core module and get fired as ECS events.
var iter = mach.core.pollEvents();
var direction = game.state().direction;
var spawning = game.state().spawning;
while (iter.next()) |event| {
@ -123,7 +118,7 @@ fn tick(
else => {},
}
},
.close => engine.send(.exit, .{}),
.close => core.send(.exit, .{}),
else => {},
}
}
@ -140,7 +135,7 @@ fn tick(
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();
const new_entity = try core.newEntity();
try sprite.set(new_entity, .transform, Mat4x4.translate(new_pos).mul(&Mat4x4.scale(Vec3.splat(0.3))));
try sprite.set(new_entity, .size, vec2(32, 32));
try sprite.set(new_entity, .uv_transform, Mat3x3.translate(vec2(0, 0)));
@ -153,7 +148,7 @@ fn tick(
const delta_time = game.state().timer.lap();
// Rotate entities
var archetypes_iter = engine.entities.query(.{ .all = &.{
var archetypes_iter = core.entities.query(.{ .all = &.{
.{ .mach_gfx_sprite = &.{.transform} },
} });
while (archetypes_iter.next()) |archetype| {
@ -187,26 +182,60 @@ fn tick(
// Perform pre-render work
sprite_pipeline.send(.pre_render, .{});
// Render a frame
engine.send(.begin_pass, .{gpu.Color{ .r = 1.0, .g = 1.0, .b = 1.0, .a = 1.0 }});
// Create a command encoder for this frame
game.state().frame_encoder = mach.core.device.createCommandEncoder(null);
// Grab the back buffer of the swapchain
const back_buffer_view = mach.core.swap_chain.getCurrentTextureView().?;
defer back_buffer_view.release();
// Begin render pass
const sky_blue = gpu.Color{ .r = 0.776, .g = 0.988, .b = 1, .a = 1 };
const color_attachments = [_]gpu.RenderPassColorAttachment{.{
.view = back_buffer_view,
.clear_value = sky_blue,
.load_op = .clear,
.store_op = .store,
}};
game.state().frame_encoder = mach.core.device.createCommandEncoder(null);
game.state().frame_render_pass = game.state().frame_encoder.beginRenderPass(&gpu.RenderPassDescriptor.init(.{
.label = "main render pass",
.color_attachments = &color_attachments,
}));
// Render our sprite batch
sprite_pipeline.state().render_pass = game.state().frame_render_pass;
sprite_pipeline.send(.render, .{});
engine.send(.end_pass, .{});
engine.send(.frame_done, .{}); // Present the frame
// Finish the frame once rendering is done.
game.send(.end_frame, .{});
game.state().time += delta_time;
}
fn endFrame(game: *Mod) !void {
// Finish render pass
game.state().frame_render_pass.end();
var command = game.state().frame_encoder.finish(null);
game.state().frame_encoder.release();
defer command.release();
mach.core.queue.submit(&[_]*gpu.CommandBuffer{command});
// Present the frame
mach.core.swap_chain.present();
// 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 });
try mach.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, allocator: std.mem.Allocator) !*gpu.Texture {
const device = engine.state().device;
fn loadTexture(core: *mach.Core.Mod, allocator: std.mem.Allocator) !*gpu.Texture {
const device = core.state().device;
const queue = device.getQueue();
// Load the image from memory

View file

@ -1,17 +1,18 @@
// TODO(important): review all code in this file in-depth
// 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.
// The global list of Mach modules registered for use in our application.
pub const modules = .{
mach.Engine,
mach.Core,
mach.gfx.Sprite,
mach.gfx.SpritePipeline,
Game,
@import("Game.zig"),
};
pub const App = mach.App;
// TODO(important): use standard entrypoint instead
pub fn main() !void {
// Initialize mach.Core
try mach.core.initModule();
// Main loop
while (try mach.core.tick()) {}
}

View file

@ -3,10 +3,8 @@ 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 Text = mach.gfx.Text;
const math = mach.math;
const vec2 = math.vec2;
@ -26,35 +24,26 @@ spawning: bool = false,
spawn_timer: mach.Timer,
fps_timer: mach.Timer,
frame_count: usize,
texts: usize,
rand: std.rand.DefaultPrng,
time: f32,
style1: mach.EntityID,
allocator: std.mem.Allocator,
pipeline: mach.EntityID,
frame_encoder: *gpu.CommandEncoder = undefined,
frame_render_pass: *gpu.RenderPassEncoder = undefined,
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.
//
// Define the globally unique name of our module. You can use any name here, but keep in mind no
// two modules in the program can have the same name.
pub const name = .game;
pub const Mod = mach.Mod(@This());
pub const global_events = .{
.init = .{ .handler = init },
.deinit = .{ .handler = deinit },
.tick = .{ .handler = tick },
};
pub const Pipeline = enum(u32) {
default,
pub const local_events = .{
.end_frame = .{ .handler = endFrame },
};
const upscale = 1.0;
@ -65,42 +54,49 @@ const text1: []const []const u8 = &.{
"bold\nand\n",
};
const text2: []const []const u8 = &.{"!$?😊"};
const text2: []const []const u8 = &.{"$!?😊"};
fn init(
engine: *mach.Engine.Mod,
text: *Text.Mod,
core: *mach.Core.Mod,
text: *gfx.Text.Mod,
text_pipeline: *gfx.TextPipeline.Mod,
text_style: *gfx.TextStyle.Mod,
game: *Mod,
) !void {
// The Mach .core is where we set window options, etc.
core.setTitle("gfx.Text example");
mach.core.setTitle("gfx.Text example");
// TODO: a better way to initialize entities with default values
const style1 = try engine.newEntity();
// TODO(text): most of these style options are not respected yet.
const style1 = try core.newEntity();
try text_style.set(style1, .font_name, "Roboto Medium"); // TODO
try text_style.set(style1, .font_size, 48 * gfx.px_per_pt); // 48pt
try text_style.set(style1, .font_weight, gfx.font_weight_normal);
try text_style.set(style1, .italic, false);
try text_style.set(style1, .color, vec4(0.6, 1.0, 0.6, 1.0));
const style2 = try engine.newEntity();
const style2 = try core.newEntity();
try text_style.set(style2, .font_name, "Roboto Medium"); // TODO
try text_style.set(style2, .font_size, 48 * gfx.px_per_pt); // 48pt
try text_style.set(style2, .font_weight, gfx.font_weight_normal);
try text_style.set(style2, .italic, true);
try text_style.set(style2, .color, vec4(0.6, 1.0, 0.6, 1.0));
const style3 = try engine.newEntity();
const style3 = try core.newEntity();
try text_style.set(style3, .font_name, "Roboto Medium"); // TODO
try text_style.set(style3, .font_size, 48 * gfx.px_per_pt); // 48pt
try text_style.set(style3, .font_weight, gfx.font_weight_bold);
try text_style.set(style3, .italic, false);
try text_style.set(style3, .color, vec4(0.6, 1.0, 0.6, 1.0));
// Create a text rendering pipeline
const pipeline = try core.newEntity();
try text_pipeline.set(pipeline, .is_pipeline, {});
text_pipeline.send(.update, .{});
// Create some text
const player = try engine.newEntity();
try text.set(player, .pipeline, @intFromEnum(Pipeline.default));
const player = try core.newEntity();
try text.set(player, .pipeline, pipeline);
try text.set(player, .transform, Mat4x4.scaleScalar(upscale).mul(&Mat4x4.translate(vec3(0, 0, 0))));
// TODO: better storage mechanism for this
@ -112,10 +108,7 @@ fn init(
styles[2] = style3;
try text.set(player, .text, text1);
try text.set(player, .style, styles);
text.send(.init_pipeline, .{Text.PipelineOptions{
.pipeline = @intFromEnum(Pipeline.default),
}});
try text.set(player, .dirty, true);
game.init(.{
.timer = try mach.Timer.start(),
@ -123,25 +116,22 @@ fn init(
.player = player,
.fps_timer = try mach.Timer.start(),
.frame_count = 0,
.texts = 0,
.rand = std.rand.DefaultPrng.init(1337),
.time = 0,
.style1 = style1,
.allocator = allocator,
.pipeline = pipeline,
});
}
fn deinit(engine: *mach.Engine.Mod) !void {
_ = engine;
}
fn tick(
engine: *mach.Engine.Mod,
text: *Text.Mod,
core: *mach.Core.Mod,
text: *gfx.Text.Mod,
text_pipeline: *gfx.TextPipeline.Mod,
game: *Mod,
) !void {
// TODO(engine): event polling should occur in mach.Engine module and get fired as ECS events.
var iter = core.pollEvents();
// TODO(important): event polling should occur in mach.Core module and get fired as ECS events.
var iter = mach.core.pollEvents();
var direction = game.state().direction;
var spawning = game.state().spawning;
while (iter.next()) |event| {
@ -166,7 +156,7 @@ fn tick(
else => {},
}
},
.close => engine.send(.exit, .{}),
.close => core.send(.exit, .{}),
else => {},
}
}
@ -175,16 +165,16 @@ fn tick(
var player_transform = text.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) {
if (spawning and game.state().spawn_timer.read() > (1.0 / 60.0)) {
// Spawn new entities
_ = game.state().spawn_timer.lap();
for (0..1) |_| {
for (0..10) |_| {
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;
new_pos.v[0] += game.state().rand.random().floatNorm(f32) * 50;
new_pos.v[1] += game.state().rand.random().floatNorm(f32) * 50;
const new_entity = try engine.newEntity();
try text.set(new_entity, .pipeline, @intFromEnum(Pipeline.default));
const new_entity = try core.newEntity();
try text.set(new_entity, .pipeline, game.state().pipeline);
try text.set(new_entity, .transform, Mat4x4.scaleScalar(upscale).mul(&Mat4x4.translate(new_pos)));
// TODO: better storage mechanism for this
@ -193,8 +183,7 @@ fn tick(
styles[0] = game.state().style1;
try text.set(new_entity, .text, text2);
try text.set(new_entity, .style, styles);
game.state().texts += 1;
try text.set(new_entity, .dirty, true);
}
}
@ -202,7 +191,7 @@ fn tick(
const delta_time = game.state().timer.lap();
// Rotate entities
var archetypes_iter = engine.entities.query(.{ .all = &.{
var archetypes_iter = core.entities.query(.{ .all = &.{
.{ .mach_gfx_text = &.{.transform} },
} });
while (archetypes_iter.next()) |archetype| {
@ -231,23 +220,75 @@ fn tick(
player_pos.v[0] += direction.x() * speed * delta_time;
player_pos.v[1] += direction.y() * speed * delta_time;
try text.set(game.state().player, .transform, Mat4x4.scaleScalar(upscale).mul(&Mat4x4.translate(player_pos)));
text.send(.updated, .{@intFromEnum(Pipeline.default)});
try text.set(game.state().player, .dirty, true);
text.send(.update, .{});
// Perform pre-render work
text.send(.pre_render, .{@intFromEnum(Pipeline.default)});
text_pipeline.send(.pre_render, .{});
// Render a frame
engine.send(.begin_pass, .{gpu.Color{ .r = 1.0, .g = 1.0, .b = 1.0, .a = 1.0 }});
text.send(.render, .{@intFromEnum(Pipeline.default)});
engine.send(.end_pass, .{});
engine.send(.frame_done, .{}); // Present the frame
// Create a command encoder for this frame
game.state().frame_encoder = mach.core.device.createCommandEncoder(null);
// Grab the back buffer of the swapchain
const back_buffer_view = mach.core.swap_chain.getCurrentTextureView().?;
defer back_buffer_view.release();
// Begin render pass
const sky_blue = gpu.Color{ .r = 0.776, .g = 0.988, .b = 1, .a = 1 };
const color_attachments = [_]gpu.RenderPassColorAttachment{.{
.view = back_buffer_view,
.clear_value = sky_blue,
.load_op = .clear,
.store_op = .store,
}};
game.state().frame_encoder = mach.core.device.createCommandEncoder(null);
game.state().frame_render_pass = game.state().frame_encoder.beginRenderPass(&gpu.RenderPassDescriptor.init(.{
.label = "main render pass",
.color_attachments = &color_attachments,
}));
// Render our text batch
text_pipeline.state().render_pass = game.state().frame_render_pass;
text_pipeline.send(.render, .{});
// Finish the frame once rendering is done.
game.send(.end_frame, .{});
game.state().time += delta_time;
}
fn endFrame(game: *Mod, text: *gfx.Text.Mod) !void {
// Finish render pass
game.state().frame_render_pass.end();
var command = game.state().frame_encoder.finish(null);
game.state().frame_encoder.release();
defer command.release();
mach.core.queue.submit(&[_]*gpu.CommandBuffer{command});
// Present the frame
mach.core.swap_chain.present();
// 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 });
// Gather some text rendering stats
var num_texts: u32 = 0;
var num_glyphs: usize = 0;
var archetypes_iter = text.entities.query(.{ .all = &.{
.{ .mach_gfx_text = &.{
.built,
} },
} });
while (archetypes_iter.next()) |archetype| {
const builts = archetype.slice(.mach_gfx_text, .built);
for (builts) |built| {
num_texts += 1;
num_glyphs += built.glyphs.items.len;
}
}
try mach.core.printTitle("gfx.Text example [ FPS: {d} ] [ Texts: {d} ] [ Glyphs: {d} ]", .{ game.state().frame_count, num_texts, num_glyphs });
game.state().fps_timer.reset();
game.state().frame_count = 0;
}
game.state().frame_count += 1;
game.state().time += delta_time;
}

View file

@ -1,17 +1,19 @@
// TODO(important): review all code in this file in-depth
// 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.
// The global list of Mach modules registered for use in our application.
pub const modules = .{
mach.Engine,
mach.Core,
mach.gfx.Text,
mach.gfx.TextPipeline,
mach.gfx.TextStyle,
Game,
@import("Game.zig"),
};
pub const App = mach.App;
// TODO(important): use standard entrypoint instead
pub fn main() !void {
// Initialize mach.Core
try mach.core.initModule();
// Main loop
while (try mach.core.tick()) {}
}

View file

@ -1,7 +1,6 @@
const std = @import("std");
const mach = @import("../main.zig");
const core = mach.core;
const gpu = mach.core.gpu;
const gpu = mach.gpu;
const gfx = mach.gfx;
const Engine = mach.Engine;
@ -44,7 +43,7 @@ pub const local_events = .{
.update = .{ .handler = update },
};
fn update(engine: *Engine.Mod, sprite: *Mod, sprite_pipeline: *gfx.SpritePipeline.Mod) !void {
fn update(core: *mach.Core.Mod, sprite: *Mod, sprite_pipeline: *gfx.SpritePipeline.Mod) !void {
var archetypes_iter = sprite_pipeline.entities.query(.{ .all = &.{
.{ .mach_gfx_sprite_pipeline = &.{
.built,
@ -54,19 +53,19 @@ fn update(engine: *Engine.Mod, sprite: *Mod, sprite_pipeline: *gfx.SpritePipelin
const ids = archetype.slice(.entity, .id);
const built_pipelines = archetype.slice(.mach_gfx_sprite_pipeline, .built);
for (ids, built_pipelines) |pipeline_id, *built| {
try updatePipeline(engine, sprite, sprite_pipeline, pipeline_id, built);
try updatePipeline(core, sprite, sprite_pipeline, pipeline_id, built);
}
}
}
fn updatePipeline(
engine: *Engine.Mod,
core: *mach.Core.Mod,
sprite: *Mod,
sprite_pipeline: *gfx.SpritePipeline.Mod,
pipeline_id: mach.EntityID,
built: *gfx.SpritePipeline.BuiltPipeline,
) !void {
const device = engine.state().device;
const device = core.state().device;
const encoder = device.createCommandEncoder(null);
defer encoder.release();
@ -110,6 +109,6 @@ fn updatePipeline(
encoder.writeBuffer(built.sizes, 0, gfx.SpritePipeline.cp_sizes[0..i]);
var command = encoder.finish(null);
defer command.release();
engine.state().queue.submit(&[_]*gpu.CommandBuffer{command});
core.state().queue.submit(&[_]*gpu.CommandBuffer{command});
}
}

View file

@ -1,6 +1,5 @@
const std = @import("std");
const mach = @import("../main.zig");
const core = mach.core;
const gpu = mach.gpu;
const math = mach.math;
@ -89,6 +88,9 @@ pub var cp_transforms: [sprite_buffer_cap]math.Mat4x4 = undefined;
pub var cp_uv_transforms: [sprite_buffer_cap]math.Mat3x3 = undefined;
pub var cp_sizes: [sprite_buffer_cap]math.Vec2 = undefined;
/// Which render pass should be used during .render
render_pass: ?*gpu.RenderPassEncoder = null,
pub const BuiltPipeline = struct {
render: *gpu.RenderPipeline,
texture_sampler: *gpu.Sampler,
@ -144,7 +146,9 @@ fn deinit(sprite_pipeline: *Mod) void {
}
}
fn update(engine: *mach.Engine.Mod, sprite_pipeline: *Mod) !void {
fn update(core: *mach.Core.Mod, sprite_pipeline: *Mod) !void {
sprite_pipeline.init(.{});
// Destroy all sprite render pipelines. We will rebuild them all.
deinit(sprite_pipeline);
@ -158,13 +162,13 @@ fn update(engine: *mach.Engine.Mod, sprite_pipeline: *Mod) !void {
const textures = archetype.slice(.mach_gfx_sprite_pipeline, .texture);
for (ids, textures) |pipeline_id, texture| {
try buildPipeline(engine, sprite_pipeline, pipeline_id, texture);
try buildPipeline(core, sprite_pipeline, pipeline_id, texture);
}
}
}
fn buildPipeline(
engine: *mach.Engine.Mod,
core: *mach.Core.Mod,
sprite_pipeline: *Mod,
pipeline_id: mach.EntityID,
texture: *gpu.Texture,
@ -181,7 +185,7 @@ fn buildPipeline(
const opt_fragment_state = sprite_pipeline.get(pipeline_id, .fragment_state);
const opt_layout = sprite_pipeline.get(pipeline_id, .layout);
const device = engine.state().device;
const device = core.state().device;
// Storage buffers
const transforms = device.createBuffer(&.{
@ -269,7 +273,7 @@ fn buildPipeline(
defer shader_module.release();
const color_target = opt_color_target_state orelse gpu.ColorTargetState{
.format = core.descriptor.format,
.format = mach.core.descriptor.format,
.blend = &blend_state,
.write_mask = gpu.ColorWriteMaskFlags.all,
};
@ -311,10 +315,12 @@ fn buildPipeline(
try sprite_pipeline.set(pipeline_id, .num_sprites, 0);
}
fn preRender(
engine: *mach.Engine.Mod,
sprite_pipeline: *Mod,
) void {
fn preRender(sprite_pipeline: *Mod) void {
const encoder = mach.core.device.createCommandEncoder(&gpu.CommandEncoder.Descriptor{
.label = "SpritePipeline.encoder",
});
defer encoder.release();
var archetypes_iter = sprite_pipeline.entities.query(.{ .all = &.{
.{ .mach_gfx_sprite_pipeline = &.{
.built,
@ -326,10 +332,10 @@ fn preRender(
// Create the projection matrix
// TODO(sprite): move this out of the hot codepath
const proj = math.Mat4x4.projection2D(.{
.left = -@as(f32, @floatFromInt(core.size().width)) / 2,
.right = @as(f32, @floatFromInt(core.size().width)) / 2,
.bottom = -@as(f32, @floatFromInt(core.size().height)) / 2,
.top = @as(f32, @floatFromInt(core.size().height)) / 2,
.left = -@as(f32, @floatFromInt(mach.core.size().width)) / 2,
.right = @as(f32, @floatFromInt(mach.core.size().width)) / 2,
.bottom = -@as(f32, @floatFromInt(mach.core.size().height)) / 2,
.top = @as(f32, @floatFromInt(mach.core.size().height)) / 2,
.near = -0.1,
.far = 100000,
});
@ -343,15 +349,20 @@ fn preRender(
@as(f32, @floatFromInt(built.texture.getHeight())),
),
};
engine.state().encoder.writeBuffer(built.uniforms, 0, &[_]Uniforms{uniforms});
encoder.writeBuffer(built.uniforms, 0, &[_]Uniforms{uniforms});
}
}
var command = encoder.finish(null);
defer command.release();
mach.core.queue.submit(&[_]*gpu.CommandBuffer{command});
}
fn render(
engine: *mach.Engine.Mod,
sprite_pipeline: *Mod,
) !void {
fn render(sprite_pipeline: *Mod) !void {
const render_pass = if (sprite_pipeline.state().render_pass) |rp| rp else std.debug.panic("sprite_pipeline.state().render_pass must be specified", .{});
sprite_pipeline.state().render_pass = null;
// TODO(sprite): need a way to specify order of rendering with multiple pipelines
var archetypes_iter = sprite_pipeline.entities.query(.{ .all = &.{
.{ .mach_gfx_sprite_pipeline = &.{
.built,
@ -362,12 +373,11 @@ fn render(
const built_pipelines = archetype.slice(.mach_gfx_sprite_pipeline, .built);
for (ids, built_pipelines) |pipeline_id, built| {
// Draw the sprite batch
const pass = engine.state().pass;
const total_vertices = sprite_pipeline.get(pipeline_id, .num_sprites).? * 6;
pass.setPipeline(built.render);
render_pass.setPipeline(built.render);
// TODO(sprite): remove dynamic offsets?
pass.setBindGroup(0, built.bind_group, &.{});
pass.draw(total_vertices, 1, 0, 0);
render_pass.setBindGroup(0, built.bind_group, &.{});
render_pass.draw(total_vertices, 1, 0, 0);
}
}
}

View file

@ -1,6 +1,5 @@
const std = @import("std");
const mach = @import("../main.zig");
const core = mach.core;
const gpu = mach.gpu;
const Engine = mach.Engine;
const gfx = mach.gfx;
@ -14,22 +13,10 @@ const vec4 = math.vec4;
const Mat3x3 = math.Mat3x3;
const Mat4x4 = math.Mat4x4;
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
/// Internal state
pipelines: std.AutoArrayHashMapUnmanaged(u32, Pipeline) = .{},
allocator: std.mem.Allocator,
pub const name = .mach_gfx_text;
pub const Mod = mach.Mod(@This());
pub const components = .{
.pipeline = .{ .type = u8, .description =
\\ The ID of the pipeline this text belongs to. By default, zero.
\\
\\ This determines which shader, textures, etc. are used for rendering the text.
},
.transform = .{ .type = Mat4x4, .description =
\\ The text model transformation matrix. Text is measured in pixel units, starting from
\\ (0, 0) at the top-left corner and extending to the size of the text. By default, the world
@ -47,370 +34,139 @@ pub const components = .{
\\
\\ Expected to match the length of the text component.
},
};
pub const global_events = .{
.deinit = .{ .handler = deinit },
.init = .{ .handler = init },
.dirty = .{ .type = bool, .description =
\\ If true, the underlying glyph buffers, texture atlas, and transform buffers will be updated
\\ as needed to reflect the latest component values.
\\
\\ This lets rendering be static if no changes have occurred.
},
.pipeline = .{ .type = mach.EntityID, .description =
\\ Which render pipeline to use for rendering the text.
\\
\\ This determines which shader, textures, etc. are used for rendering the text.
},
.built = .{ .type = BuiltText, .description = "internal" },
};
pub const local_events = .{
.init_pipeline = .{ .handler = initPipeline },
.updated = .{ .handler = updated },
.pre_render = .{ .handler = preRender },
.render = .{ .handler = render },
.update = .{ .handler = update },
};
const Uniforms = extern struct {
// WebGPU requires that the size of struct fields are multiples of 16
// So we use align(16) and 'extern' to maintain field order
/// The view * orthographic projection matrix
view_projection: Mat4x4 align(16),
/// Total size of the font atlas texture in pixels
texture_size: Vec2 align(16),
const BuiltText = struct {
glyphs: std.ArrayListUnmanaged(gfx.TextPipeline.Glyph),
};
const Glyph = extern struct {
/// Position of this glyph (top-left corner.)
pos: Vec2,
/// Width of the glyph in pixels.
size: Vec2,
/// Normalized position of the top-left UV coordinate
uv_pos: Vec2,
/// Which text this glyph belongs to; this is the index for transforms[i], colors[i].
text_index: u32,
};
const GlyphKey = struct {
index: u32,
// Auto Hashing doesn't work for floats, so we bitcast to integer.
size: u32,
};
const RegionMap = std.AutoArrayHashMapUnmanaged(GlyphKey, gfx.Atlas.Region);
const Pipeline = struct {
render: *gpu.RenderPipeline,
texture_sampler: *gpu.Sampler,
texture: *gpu.Texture,
texture_atlas: gfx.Atlas,
texture2: ?*gpu.Texture,
texture3: ?*gpu.Texture,
texture4: ?*gpu.Texture,
bind_group: *gpu.BindGroup,
uniforms: *gpu.Buffer,
regions: RegionMap = .{},
// Storage buffers
num_texts: u32,
num_glyphs: u32,
transforms: *gpu.Buffer,
colors: *gpu.Buffer,
glyphs: *gpu.Buffer,
pub fn reference(p: *Pipeline) void {
p.render.reference();
p.texture_sampler.reference();
p.texture.reference();
if (p.texture2) |tex| tex.reference();
if (p.texture3) |tex| tex.reference();
if (p.texture4) |tex| tex.reference();
p.bind_group.reference();
p.uniforms.reference();
p.transforms.reference();
p.colors.reference();
p.glyphs.reference();
}
pub fn deinit(p: *Pipeline, allocator: std.mem.Allocator) void {
p.render.release();
p.texture_sampler.release();
p.texture.release();
p.texture_atlas.deinit(allocator);
if (p.texture2) |tex| tex.release();
if (p.texture3) |tex| tex.release();
if (p.texture4) |tex| tex.release();
p.bind_group.release();
p.uniforms.release();
p.regions.deinit(allocator);
p.transforms.release();
p.colors.release();
p.glyphs.release();
}
};
pub const PipelineOptions = struct {
pipeline: u32,
/// Shader program to use when rendering.
shader: ?*gpu.ShaderModule = null,
/// Whether to use linear (blurry) or nearest (pixelated) upscaling/downscaling.
texture_sampler: ?*gpu.Sampler = null,
/// Textures to use when rendering. The default shader can handle one texture (the font atlas.)
texture2: ?*gpu.Texture = null,
texture3: ?*gpu.Texture = null,
texture4: ?*gpu.Texture = null,
/// Alpha and color blending options.
blend_state: ?gpu.BlendState = null,
/// Pipeline overrides, these can be used to e.g. pass additional things to your shader program.
bind_group_layout: ?*gpu.BindGroupLayout = null,
bind_group: ?*gpu.BindGroup = null,
color_target_state: ?gpu.ColorTargetState = null,
fragment_state: ?gpu.FragmentState = null,
pipeline_layout: ?*gpu.PipelineLayout = null,
};
fn deinit(text: *Mod) !void {
for (text.state().pipelines.entries.items(.value)) |*pipeline| pipeline.deinit(text.state().allocator);
text.state().pipelines.deinit(text.state().allocator);
}
fn init(text: *Mod) void {
text.init(.{
.allocator = gpa.allocator(),
});
}
// TODO(text): no args
fn initPipeline(
engine: *Engine.Mod,
text: *Mod,
opt: PipelineOptions,
) !void {
const device = engine.state().device;
const pipeline = try text.state().pipelines.getOrPut(text.state().allocator, opt.pipeline);
if (pipeline.found_existing) {
pipeline.value_ptr.*.deinit(text.state().allocator);
}
// Prepare texture for the font atlas.
const img_size = gpu.Extent3D{ .width = 1024, .height = 1024 };
const texture = device.createTexture(&.{
.size = img_size,
.format = .rgba8_unorm,
.usage = .{
.texture_binding = true,
.copy_dst = true,
.render_attachment = true,
},
});
const texture_atlas = try gfx.Atlas.init(
text.state().allocator,
img_size.width,
.rgba,
);
// Storage buffers
const buffer_cap = 1024 * 128; // TODO: allow user to specify preallocation
const glyph_buffer_cap = 1024 * 512; // TODO: allow user to specify preallocation
const transforms = device.createBuffer(&.{
.usage = .{ .storage = true, .copy_dst = true },
.size = @sizeOf(Mat4x4) * buffer_cap,
.mapped_at_creation = .false,
});
const colors = device.createBuffer(&.{
.usage = .{ .storage = true, .copy_dst = true },
.size = @sizeOf(Vec4) * buffer_cap,
.mapped_at_creation = .false,
});
const glyphs = device.createBuffer(&.{
.usage = .{ .storage = true, .copy_dst = true },
.size = @sizeOf(Glyph) * glyph_buffer_cap,
.mapped_at_creation = .false,
});
const texture_sampler = opt.texture_sampler orelse device.createSampler(&.{
.mag_filter = .nearest,
.min_filter = .nearest,
});
const uniforms = device.createBuffer(&.{
.usage = .{ .copy_dst = true, .uniform = true },
.size = @sizeOf(Uniforms),
.mapped_at_creation = .false,
});
const bind_group_layout = opt.bind_group_layout orelse device.createBindGroupLayout(
&gpu.BindGroupLayout.Descriptor.init(.{
.entries = &.{
gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true }, .uniform, false, 0),
gpu.BindGroupLayout.Entry.buffer(1, .{ .vertex = true }, .read_only_storage, false, 0),
gpu.BindGroupLayout.Entry.buffer(2, .{ .vertex = true }, .read_only_storage, false, 0),
gpu.BindGroupLayout.Entry.buffer(3, .{ .vertex = true }, .read_only_storage, false, 0),
gpu.BindGroupLayout.Entry.sampler(4, .{ .fragment = true }, .filtering),
gpu.BindGroupLayout.Entry.texture(5, .{ .fragment = true }, .float, .dimension_2d, false),
gpu.BindGroupLayout.Entry.texture(6, .{ .fragment = true }, .float, .dimension_2d, false),
gpu.BindGroupLayout.Entry.texture(7, .{ .fragment = true }, .float, .dimension_2d, false),
gpu.BindGroupLayout.Entry.texture(8, .{ .fragment = true }, .float, .dimension_2d, false),
},
}),
);
defer bind_group_layout.release();
const texture_view = texture.createView(&gpu.TextureView.Descriptor{});
const texture2_view = if (opt.texture2) |tex| tex.createView(&gpu.TextureView.Descriptor{}) else texture_view;
const texture3_view = if (opt.texture3) |tex| tex.createView(&gpu.TextureView.Descriptor{}) else texture_view;
const texture4_view = if (opt.texture4) |tex| tex.createView(&gpu.TextureView.Descriptor{}) else texture_view;
defer texture_view.release();
defer texture2_view.release();
defer texture3_view.release();
defer texture4_view.release();
const bind_group = opt.bind_group orelse device.createBindGroup(
&gpu.BindGroup.Descriptor.init(.{
.layout = bind_group_layout,
.entries = &.{
gpu.BindGroup.Entry.buffer(0, uniforms, 0, @sizeOf(Uniforms)),
gpu.BindGroup.Entry.buffer(1, transforms, 0, @sizeOf(Mat4x4) * buffer_cap),
gpu.BindGroup.Entry.buffer(2, colors, 0, @sizeOf(Vec4) * buffer_cap),
gpu.BindGroup.Entry.buffer(3, glyphs, 0, @sizeOf(Glyph) * glyph_buffer_cap),
gpu.BindGroup.Entry.sampler(4, texture_sampler),
gpu.BindGroup.Entry.textureView(5, texture_view),
gpu.BindGroup.Entry.textureView(6, texture2_view),
gpu.BindGroup.Entry.textureView(7, texture3_view),
gpu.BindGroup.Entry.textureView(8, texture4_view),
},
}),
);
const blend_state = opt.blend_state orelse 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 shader_module = opt.shader orelse device.createShaderModuleWGSL("text.wgsl", @embedFile("text.wgsl"));
defer shader_module.release();
const color_target = opt.color_target_state orelse gpu.ColorTargetState{
.format = core.descriptor.format,
.blend = &blend_state,
.write_mask = gpu.ColorWriteMaskFlags.all,
};
const fragment = opt.fragment_state orelse gpu.FragmentState.init(.{
.module = shader_module,
.entry_point = "fragMain",
.targets = &.{color_target},
});
const bind_group_layouts = [_]*gpu.BindGroupLayout{bind_group_layout};
const pipeline_layout = opt.pipeline_layout orelse device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{
.bind_group_layouts = &bind_group_layouts,
}));
defer pipeline_layout.release();
const render_pipeline = device.createRenderPipeline(&gpu.RenderPipeline.Descriptor{
.fragment = &fragment,
.layout = pipeline_layout,
.vertex = gpu.VertexState{
.module = shader_module,
.entry_point = "vertMain",
},
});
pipeline.value_ptr.* = Pipeline{
.render = render_pipeline,
.texture_sampler = texture_sampler,
.texture = texture,
.texture_atlas = texture_atlas,
.texture2 = opt.texture2,
.texture3 = opt.texture3,
.texture4 = opt.texture4,
.bind_group = bind_group,
.uniforms = uniforms,
.num_texts = 0,
.num_glyphs = 0,
.transforms = transforms,
.colors = colors,
.glyphs = glyphs,
};
pipeline.value_ptr.reference();
}
// TODO(text): no args
fn updated(
engine: *Engine.Mod,
text: *Mod,
pipeline_id: u32,
) !void {
const pipeline = text.state().pipelines.getPtr(pipeline_id).?;
const device = engine.state().device;
// TODO: make sure these entities only belong to the given pipeline
// we need a better tagging mechanism
var archetypes_iter = engine.entities.query(.{ .all = &.{
.{ .mach_gfx_text = &.{
.pipeline,
.transform,
.text,
fn update(core: *mach.Core.Mod, text: *Mod, text_pipeline: *gfx.TextPipeline.Mod) !void {
var archetypes_iter = text_pipeline.entities.query(.{ .all = &.{
.{ .mach_gfx_text_pipeline = &.{
.built,
} },
} });
while (archetypes_iter.next()) |archetype| {
const ids = archetype.slice(.entity, .id);
const built_pipelines = archetype.slice(.mach_gfx_text_pipeline, .built);
for (ids, built_pipelines) |pipeline_id, *built| {
try updatePipeline(core, text, text_pipeline, pipeline_id, built);
}
}
}
fn updatePipeline(
core: *mach.Core.Mod,
text: *Mod,
text_pipeline: *gfx.TextPipeline.Mod,
pipeline_id: mach.EntityID,
built: *gfx.TextPipeline.BuiltPipeline,
) !void {
const device = core.state().device;
const encoder = device.createCommandEncoder(null);
defer encoder.release();
pipeline.num_texts = 0;
pipeline.num_glyphs = 0;
var glyphs = std.ArrayListUnmanaged(Glyph){};
var transforms_offset: usize = 0;
const allocator = text_pipeline.state().allocator;
var glyphs = if (text_pipeline.state().glyph_update_buffer) |*b| b else blk: {
// TODO(text): better default allocation size
const b = try std.ArrayListUnmanaged(gfx.TextPipeline.Glyph).initCapacity(allocator, 256);
text_pipeline.state().glyph_update_buffer = b;
break :blk &text_pipeline.state().glyph_update_buffer.?;
};
glyphs.clearRetainingCapacity();
var texture_update = false;
var num_texts: u32 = 0;
var removes = try std.ArrayListUnmanaged(mach.EntityID).initCapacity(allocator, 8);
var archetypes_iter = text.entities.query(.{ .all = &.{
.{ .mach_gfx_text = &.{
.transform,
.text,
.style,
.pipeline,
} },
} });
while (archetypes_iter.next()) |archetype| {
const ids = archetype.slice(.entity, .id);
const transforms = archetype.slice(.mach_gfx_text, .transform);
const segment_slices = archetype.slice(.mach_gfx_text, .text);
const style_slices = archetype.slice(.mach_gfx_text, .style);
const pipelines = archetype.slice(.mach_gfx_text, .pipeline);
// TODO: confirm the lifetime of these slices is OK for writeBuffer, how long do they need
// to live?
encoder.writeBuffer(pipeline.transforms, transforms_offset, transforms);
// encoder.writeBuffer(pipeline.colors, colors_offset, colors);
// TODO: currently we cannot query all texts which have a _single_ pipeline component
// value and get back contiguous memory for all of them. This is because all texts with
// possibly different pipeline component values are stored as the same archetype. If we
// introduce a new concept of tagging-by-value to our entity storage then we can enforce
// that all entities with the same pipeline value are stored in contiguous memory, and
// skip this copy.
for (ids, transforms, segment_slices, style_slices, pipelines) |id, transform, segments, styles, text_pipeline_id| {
if (text_pipeline_id != pipeline_id) continue;
transforms_offset += transforms.len;
// colors_offset += colors.len;
pipeline.num_texts += @intCast(transforms.len);
gfx.TextPipeline.cp_transforms[num_texts] = transform;
// Render texts
// TODO: this is very expensive and shouldn't be done here, should be done only on detected
// text change.
const px_density = 2.0;
const segment_lists = archetype.slice(.mach_gfx_text, .text);
const style_lists = archetype.slice(.mach_gfx_text, .style);
for (segment_lists, style_lists) |segments, styles| {
if (text.get(id, .dirty) == null) {
// We do not need to rebuild this specific entity, so use cached glyph information
// from its previous build.
const built_text = text.get(id, .built).?;
for (built_text.glyphs.items) |*glyph| glyph.text_index = num_texts;
try glyphs.appendSlice(allocator, built_text.glyphs.items);
num_texts += 1;
continue;
}
// Where we will store the built glyphs for this text entity.
var built_text = if (text.get(id, .built)) |bt| bt else BuiltText{
// TODO: better default allocations
.glyphs = try std.ArrayListUnmanaged(gfx.TextPipeline.Glyph).initCapacity(allocator, 64),
};
built_text.glyphs.clearRetainingCapacity();
const px_density = 2.0; // TODO(text): do not hard-code pixel density
var origin_x: f32 = 0.0;
var origin_y: f32 = 0.0;
for (segments, styles) |segment, style| {
// Load a font
const font_name = engine.entities.getComponent(style, .mach_gfx_text_style, .font_name).?;
// Load the font
// TODO(text): allow specifying a font
// TODO(text): keep fonts around for reuse later
const font_name = core.entities.getComponent(style, .mach_gfx_text_style, .font_name).?;
_ = font_name; // TODO: actually use font name
const font_bytes = @import("font-assets").fira_sans_regular_ttf;
var font = try gfx.Font.initBytes(font_bytes);
defer font.deinit(text.state().allocator);
defer font.deinit(allocator);
const font_size = engine.entities.getComponent(style, .mach_gfx_text_style, .font_size).?;
const font_weight = engine.entities.getComponent(style, .mach_gfx_text_style, .font_weight);
const italic = engine.entities.getComponent(style, .mach_gfx_text_style, .italic);
const color = engine.entities.getComponent(style, .mach_gfx_text_style, .color);
// TODO: actually apply these
_ = font_weight;
_ = italic;
_ = color;
// TODO(text): respect these style parameters
const font_size = core.entities.getComponent(style, .mach_gfx_text_style, .font_size).?;
const font_weight = core.entities.getComponent(style, .mach_gfx_text_style, .font_weight);
_ = font_weight; // autofix
const italic = core.entities.getComponent(style, .mach_gfx_text_style, .italic);
_ = italic; // autofix
const color = core.entities.getComponent(style, .mach_gfx_text_style, .color);
_ = color; // autofix
// Create a text shaper
var run = try gfx.TextRun.init();
run.font_size_px = font_size;
run.px_density = 2; // TODO
run.px_density = px_density;
defer run.deinit();
run.addText(segment);
@ -419,18 +175,23 @@ fn updated(
while (run.next()) |glyph| {
const codepoint = segment[glyph.cluster];
// TODO: use flags(?) to detect newline, or at least something more reliable?
if (codepoint != '\n') {
const region = try pipeline.regions.getOrPut(text.state().allocator, .{
if (codepoint == '\n') {
origin_x = 0;
origin_y -= font_size;
continue;
}
const region = try built.regions.getOrPut(allocator, .{
.index = glyph.glyph_index,
.size = @bitCast(font_size),
});
if (!region.found_existing) {
const rendered_glyph = try font.render(text.state().allocator, glyph.glyph_index, .{
const rendered_glyph = try font.render(allocator, glyph.glyph_index, .{
.font_size_px = run.font_size_px,
});
if (rendered_glyph.bitmap) |bitmap| {
var glyph_atlas_region = try pipeline.texture_atlas.reserve(text.state().allocator, rendered_glyph.width, rendered_glyph.height);
pipeline.texture_atlas.set(glyph_atlas_region, @as([*]const u8, @ptrCast(bitmap.ptr))[0 .. bitmap.len * 4]);
var glyph_atlas_region = try built.texture_atlas.reserve(allocator, rendered_glyph.width, rendered_glyph.height);
built.texture_atlas.set(glyph_atlas_region, @as([*]const u8, @ptrCast(bitmap.ptr))[0 .. bitmap.len * 4]);
texture_update = true;
// Exclude the 1px blank space margin when describing the region of the texture
@ -454,96 +215,66 @@ fn updated(
const r = region.value_ptr.*;
const size = vec2(@floatFromInt(r.width), @floatFromInt(r.height));
try glyphs.append(text.state().allocator, .{
try built_text.glyphs.append(allocator, .{
.pos = vec2(
origin_x + glyph.offset.x(),
origin_y - (size.y() - glyph.offset.y()),
).divScalar(px_density),
.size = size.divScalar(px_density),
.text_index = 0,
.text_index = num_texts,
.uv_pos = vec2(@floatFromInt(r.x), @floatFromInt(r.y)),
});
pipeline.num_glyphs += 1;
}
if (codepoint == '\n') {
origin_x = 0;
origin_y -= font_size;
} else {
origin_x += glyph.advance.x();
}
}
// Update the text entity's built form
try text.set(id, .built, built_text);
// TODO(text): see below
// try text.remove(id, .dirty);
try removes.append(allocator, id);
// Add to the entire set of glyphs for this pipeline
try glyphs.appendSlice(allocator, built_text.glyphs.items);
num_texts += 1;
}
// TODO(important): removing components within an iter() currently produces undefined behavior
// (entity may exist in current iteration, plus a future iteration as the iterator moves
// on to the next archetype where the entity is now located.)
for (removes.items) |remove_id| {
try text.remove(remove_id, .dirty);
}
}
// TODO: could writeBuffer check for zero?
if (glyphs.items.len > 0) encoder.writeBuffer(pipeline.glyphs, 0, glyphs.items);
defer glyphs.deinit(text.state().allocator);
// Every pipeline update, we copy updated glyph and text buffers to the GPU.
try text_pipeline.set(pipeline_id, .num_texts, num_texts);
try text_pipeline.set(pipeline_id, .num_glyphs, @intCast(glyphs.items.len));
if (glyphs.items.len > 0) encoder.writeBuffer(built.glyphs, 0, glyphs.items);
if (num_texts > 0) {
encoder.writeBuffer(built.transforms, 0, gfx.TextPipeline.cp_transforms[0..num_texts]);
}
if (texture_update) {
// rgba32_pixels
// TODO: use proper texture dimensions here
// TODO(text): do not assume texture's data_layout and img_size here, instead get it from
// somewhere known to be matching the actual texture.
//
// TODO(text): allow users to specify RGBA32 or other pixel formats
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)),
};
engine.state().queue.writeTexture(
&.{ .texture = pipeline.texture },
core.state().queue.writeTexture(
&.{ .texture = built.texture },
&data_layout,
&img_size,
pipeline.texture_atlas.data,
built.texture_atlas.data,
);
}
if (num_texts > 0 or glyphs.items.len > 0) {
var command = encoder.finish(null);
defer command.release();
engine.state().queue.submit(&[_]*gpu.CommandBuffer{command});
}
// TODO(text): no args
fn preRender(
engine: *Engine.Mod,
text: *Mod,
pipeline_id: u32,
) !void {
const pipeline = text.state().pipelines.get(pipeline_id).?;
// Update uniform buffer
const proj = Mat4x4.projection2D(.{
.left = -@as(f32, @floatFromInt(core.size().width)) / 2,
.right = @as(f32, @floatFromInt(core.size().width)) / 2,
.bottom = -@as(f32, @floatFromInt(core.size().height)) / 2,
.top = @as(f32, @floatFromInt(core.size().height)) / 2,
.near = -0.1,
.far = 100000,
});
const uniforms = Uniforms{
.view_projection = proj,
// TODO: dimensions of other textures, number of textures present
.texture_size = vec2(
@as(f32, @floatFromInt(pipeline.texture.getWidth())),
@as(f32, @floatFromInt(pipeline.texture.getHeight())),
),
};
engine.state().encoder.writeBuffer(pipeline.uniforms, 0, &[_]Uniforms{uniforms});
}
// TODO(text): no args
fn render(
engine: *Engine.Mod,
text: *Mod,
pipeline_id: u32,
) !void {
const pipeline = text.state().pipelines.get(pipeline_id).?;
// Draw the text batch
const pass = engine.state().pass;
const total_vertices = pipeline.num_glyphs * 6;
pass.setPipeline(pipeline.render);
// TODO: remove dynamic offsets?
pass.setBindGroup(0, pipeline.bind_group, &.{});
pass.draw(total_vertices, 1, 0, 0);
core.state().queue.submit(&[_]*gpu.CommandBuffer{command});
}
}

407
src/gfx/TextPipeline.zig Normal file
View file

@ -0,0 +1,407 @@
const std = @import("std");
const mach = @import("../main.zig");
const gfx = mach.gfx;
const gpu = mach.gpu;
const math = mach.math;
pub const name = .mach_gfx_text_pipeline;
pub const Mod = mach.Mod(@This());
pub const components = .{
.is_pipeline = .{ .type = void, .description =
\\ Tag to indicate an entity represents a text pipeline.
},
.shader = .{ .type = *gpu.ShaderModule, .description =
\\ Shader program to use when rendering
\\ Defaults to text.wgsl
},
.texture_sampler = .{ .type = *gpu.Sampler, .description =
\\ Whether to use linear (blurry) or nearest (pixelated) upscaling/downscaling.
\\ Defaults to nearest (pixelated)
},
.blend_state = .{ .type = gpu.BlendState, .description =
\\ Alpha and color blending options
\\ Defaults to
\\ .{
\\ .color = .{ .operation = .add, .src_factor = .src_alpha .dst_factor = .one_minus_src_alpha },
\\ .alpha = .{ .operation = .add, .src_factor = .one, .dst_factor = .zero },
\\ }
},
.bind_group_layout = .{ .type = *gpu.BindGroupLayout, .description =
\\ Override to enable passing additional data to your shader program.
},
.bind_group = .{ .type = *gpu.BindGroup, .description =
\\ Override to enable passing additional data to your shader program.
},
.color_target_state = .{ .type = gpu.ColorTargetState, .description =
\\ Override to enable custom color target state for render pipeline.
},
.fragment_state = .{ .type = gpu.FragmentState, .description =
\\ Override to enable custom fragment state for render pipeline.
},
.layout = .{ .type = *gpu.PipelineLayout, .description =
\\ Override to enable custom pipeline layout.
},
.num_texts = .{ .type = u32, .description =
\\ Number of texts this pipeline will render.
\\ Read-only, updated as part of Text.update
},
.num_glyphs = .{ .type = u32, .description =
\\ Number of glyphs this pipeline will render.
\\ Read-only, updated as part of Text.update
},
.built = .{ .type = BuiltPipeline, .description = "internal" },
};
pub const global_events = .{
.deinit = .{ .handler = deinit },
};
pub const local_events = .{
.update = .{ .handler = update },
.pre_render = .{ .handler = preRender },
.render = .{ .handler = render },
};
const Uniforms = extern struct {
// WebGPU requires that the size of struct fields are multiples of 16
// So we use align(16) and 'extern' to maintain field order
/// The view * orthographic projection matrix
view_projection: math.Mat4x4 align(16),
/// Total size of the font atlas texture in pixels
texture_size: math.Vec2 align(16),
};
const texts_buffer_cap = 1024 * 512; // TODO(text): allow user to specify preallocation
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
// TODO(text): eliminate these, see Text.updatePipeline for details on why these exist
// currently.
pub var cp_transforms: [texts_buffer_cap]math.Mat4x4 = undefined;
pub var cp_colors: [texts_buffer_cap]math.Vec4 = undefined;
pub var cp_glyphs: [texts_buffer_cap]Glyph = undefined;
/// Which render pass should be used during .render
render_pass: ?*gpu.RenderPassEncoder = null,
glyph_update_buffer: ?std.ArrayListUnmanaged(Glyph) = null,
allocator: std.mem.Allocator,
pub const Glyph = extern struct {
/// Position of this glyph (top-left corner.)
pos: math.Vec2,
/// Width of the glyph in pixels.
size: math.Vec2,
/// Normalized position of the top-left UV coordinate
uv_pos: math.Vec2,
/// Which text this glyph belongs to; this is the index for transforms[i], colors[i].
text_index: u32,
};
const GlyphKey = struct {
index: u32,
// Auto Hashing doesn't work for floats, so we bitcast to integer.
size: u32,
};
const RegionMap = std.AutoArrayHashMapUnmanaged(GlyphKey, gfx.Atlas.Region);
pub const BuiltPipeline = struct {
render: *gpu.RenderPipeline,
texture_sampler: *gpu.Sampler,
texture: *gpu.Texture,
bind_group: *gpu.BindGroup,
uniforms: *gpu.Buffer,
texture_atlas: gfx.Atlas,
regions: RegionMap = .{},
// Storage buffers
transforms: *gpu.Buffer,
colors: *gpu.Buffer,
glyphs: *gpu.Buffer,
pub fn reference(p: *BuiltPipeline) void {
p.render.reference();
p.texture_sampler.reference();
p.texture.reference();
p.bind_group.reference();
p.uniforms.reference();
p.transforms.reference();
p.colors.reference();
p.glyphs.reference();
}
pub fn deinit(p: *BuiltPipeline, allocator: std.mem.Allocator) void {
p.render.release();
p.texture_sampler.release();
p.texture.release();
p.bind_group.release();
p.uniforms.release();
p.transforms.release();
p.colors.release();
p.glyphs.release();
p.texture_atlas.deinit(allocator);
p.regions.deinit(allocator);
}
};
fn deinit(text_pipeline: *Mod) void {
var archetypes_iter = text_pipeline.entities.query(.{ .all = &.{
.{ .mach_gfx_text_pipeline = &.{
.built,
} },
} });
while (archetypes_iter.next()) |archetype| {
for (archetype.slice(.mach_gfx_text_pipeline, .built)) |*p| p.deinit(text_pipeline.state().allocator);
}
}
fn update(core: *mach.Core.Mod, text_pipeline: *Mod) !void {
text_pipeline.init(.{
.allocator = gpa.allocator(),
});
// Destroy all text render pipelines. We will rebuild them all.
deinit(text_pipeline);
var archetypes_iter = text_pipeline.entities.query(.{ .all = &.{
.{ .mach_gfx_text_pipeline = &.{
.is_pipeline,
} },
} });
while (archetypes_iter.next()) |archetype| {
const ids = archetype.slice(.entity, .id);
for (ids) |pipeline_id| {
try buildPipeline(core, text_pipeline, pipeline_id);
}
}
}
fn buildPipeline(
core: *mach.Core.Mod,
text_pipeline: *Mod,
pipeline_id: mach.EntityID,
) !void {
const opt_shader = text_pipeline.get(pipeline_id, .shader);
const opt_texture_sampler = text_pipeline.get(pipeline_id, .texture_sampler);
const opt_blend_state = text_pipeline.get(pipeline_id, .blend_state);
const opt_bind_group_layout = text_pipeline.get(pipeline_id, .bind_group_layout);
const opt_bind_group = text_pipeline.get(pipeline_id, .bind_group);
const opt_color_target_state = text_pipeline.get(pipeline_id, .color_target_state);
const opt_fragment_state = text_pipeline.get(pipeline_id, .fragment_state);
const opt_layout = text_pipeline.get(pipeline_id, .layout);
const device = core.state().device;
// Prepare texture for the font atlas.
// TODO(text): dynamic texture re-allocation when not large enough
// TODO(text): better default allocation size
const img_size = gpu.Extent3D{ .width = 1024, .height = 1024 };
const texture = device.createTexture(&.{
.size = img_size,
.format = .rgba8_unorm,
.usage = .{
.texture_binding = true,
.copy_dst = true,
.render_attachment = true,
},
});
const texture_atlas = try gfx.Atlas.init(
text_pipeline.state().allocator,
img_size.width,
.rgba,
);
// Storage buffers
const transforms = device.createBuffer(&.{
.usage = .{ .storage = true, .copy_dst = true },
.size = @sizeOf(math.Mat4x4) * texts_buffer_cap,
.mapped_at_creation = .false,
});
const colors = device.createBuffer(&.{
.usage = .{ .storage = true, .copy_dst = true },
.size = @sizeOf(math.Vec4) * texts_buffer_cap,
.mapped_at_creation = .false,
});
const glyphs = device.createBuffer(&.{
.usage = .{ .storage = true, .copy_dst = true },
.size = @sizeOf(Glyph) * texts_buffer_cap,
.mapped_at_creation = .false,
});
const texture_sampler = opt_texture_sampler orelse device.createSampler(&.{
.mag_filter = .nearest,
.min_filter = .nearest,
});
const uniforms = device.createBuffer(&.{
.usage = .{ .copy_dst = true, .uniform = true },
.size = @sizeOf(Uniforms),
.mapped_at_creation = .false,
});
const bind_group_layout = opt_bind_group_layout orelse device.createBindGroupLayout(
&gpu.BindGroupLayout.Descriptor.init(.{
.entries = &.{
gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true }, .uniform, false, 0),
gpu.BindGroupLayout.Entry.buffer(1, .{ .vertex = true }, .read_only_storage, false, 0),
gpu.BindGroupLayout.Entry.buffer(2, .{ .vertex = true }, .read_only_storage, false, 0),
gpu.BindGroupLayout.Entry.buffer(3, .{ .vertex = true }, .read_only_storage, false, 0),
gpu.BindGroupLayout.Entry.sampler(4, .{ .fragment = true }, .filtering),
gpu.BindGroupLayout.Entry.texture(5, .{ .fragment = true }, .float, .dimension_2d, false),
},
}),
);
defer bind_group_layout.release();
const texture_view = texture.createView(&gpu.TextureView.Descriptor{});
defer texture_view.release();
const bind_group = opt_bind_group orelse device.createBindGroup(
&gpu.BindGroup.Descriptor.init(.{
.layout = bind_group_layout,
.entries = &.{
gpu.BindGroup.Entry.buffer(0, uniforms, 0, @sizeOf(Uniforms)),
gpu.BindGroup.Entry.buffer(1, transforms, 0, @sizeOf(math.Mat4x4) * texts_buffer_cap),
gpu.BindGroup.Entry.buffer(2, colors, 0, @sizeOf(math.Vec4) * texts_buffer_cap),
gpu.BindGroup.Entry.buffer(3, glyphs, 0, @sizeOf(Glyph) * texts_buffer_cap),
gpu.BindGroup.Entry.sampler(4, texture_sampler),
gpu.BindGroup.Entry.textureView(5, texture_view),
},
}),
);
const blend_state = opt_blend_state orelse 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 shader_module = opt_shader orelse device.createShaderModuleWGSL("text.wgsl", @embedFile("text.wgsl"));
defer shader_module.release();
const color_target = opt_color_target_state orelse gpu.ColorTargetState{
.format = mach.core.descriptor.format,
.blend = &blend_state,
.write_mask = gpu.ColorWriteMaskFlags.all,
};
const fragment = opt_fragment_state orelse gpu.FragmentState.init(.{
.module = shader_module,
.entry_point = "fragMain",
.targets = &.{color_target},
});
const bind_group_layouts = [_]*gpu.BindGroupLayout{bind_group_layout};
const pipeline_layout = opt_layout orelse device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{
.bind_group_layouts = &bind_group_layouts,
}));
defer pipeline_layout.release();
const render_pipeline = device.createRenderPipeline(&gpu.RenderPipeline.Descriptor{
.fragment = &fragment,
.layout = pipeline_layout,
.vertex = gpu.VertexState{
.module = shader_module,
.entry_point = "vertMain",
},
});
var built = BuiltPipeline{
.render = render_pipeline,
.texture_sampler = texture_sampler,
.texture = texture,
.bind_group = bind_group,
.uniforms = uniforms,
.transforms = transforms,
.colors = colors,
.glyphs = glyphs,
.texture_atlas = texture_atlas,
};
built.reference();
try text_pipeline.set(pipeline_id, .built, built);
try text_pipeline.set(pipeline_id, .num_texts, 0);
try text_pipeline.set(pipeline_id, .num_glyphs, 0);
}
fn preRender(text_pipeline: *Mod) void {
const encoder = mach.core.device.createCommandEncoder(&gpu.CommandEncoder.Descriptor{
.label = "TextPipeline.encoder",
});
defer encoder.release();
var archetypes_iter = text_pipeline.entities.query(.{ .all = &.{
.{ .mach_gfx_text_pipeline = &.{
.built,
} },
} });
while (archetypes_iter.next()) |archetype| {
const built_pipelines = archetype.slice(.mach_gfx_text_pipeline, .built);
for (built_pipelines) |built| {
// Create the projection matrix
// TODO(text): move this out of the hot codepath
const proj = math.Mat4x4.projection2D(.{
.left = -@as(f32, @floatFromInt(mach.core.size().width)) / 2,
.right = @as(f32, @floatFromInt(mach.core.size().width)) / 2,
.bottom = -@as(f32, @floatFromInt(mach.core.size().height)) / 2,
.top = @as(f32, @floatFromInt(mach.core.size().height)) / 2,
.near = -0.1,
.far = 100000,
});
// Update uniform buffer
const uniforms = Uniforms{
.view_projection = proj,
// TODO(text): dimensions of other textures, number of textures present
.texture_size = math.vec2(
@as(f32, @floatFromInt(built.texture.getWidth())),
@as(f32, @floatFromInt(built.texture.getHeight())),
),
};
encoder.writeBuffer(built.uniforms, 0, &[_]Uniforms{uniforms});
}
}
var command = encoder.finish(null);
defer command.release();
mach.core.queue.submit(&[_]*gpu.CommandBuffer{command});
}
fn render(text_pipeline: *Mod) !void {
const render_pass = if (text_pipeline.state().render_pass) |rp| rp else std.debug.panic("text_pipeline.state().render_pass must be specified", .{});
text_pipeline.state().render_pass = null;
// TODO(text): need a way to specify order of rendering with multiple pipelines
var archetypes_iter = text_pipeline.entities.query(.{ .all = &.{
.{ .mach_gfx_text_pipeline = &.{
.built,
} },
} });
while (archetypes_iter.next()) |archetype| {
const ids = archetype.slice(.entity, .id);
const built_pipelines = archetype.slice(.mach_gfx_text_pipeline, .built);
for (ids, built_pipelines) |pipeline_id, built| {
// Draw the text batch
const total_vertices = text_pipeline.get(pipeline_id, .num_glyphs).? * 6;
render_pass.setPipeline(built.render);
// TODO(text): remove dynamic offsets?
render_pass.setBindGroup(0, built.bind_group, &.{});
render_pass.draw(total_vertices, 1, 0, 0);
}
}
}

View file

@ -5,6 +5,7 @@ pub const Atlas = @import("atlas/Atlas.zig");
pub const Sprite = @import("Sprite.zig");
pub const SpritePipeline = @import("SpritePipeline.zig");
pub const Text = @import("Text.zig");
pub const TextPipeline = @import("TextPipeline.zig");
pub const TextStyle = @import("TextStyle.zig");
// Fonts