{gfx,examples}: update all to new mach.Core module API
Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
This commit is contained in:
parent
ac4fe65eb2
commit
d045b34f70
13 changed files with 910 additions and 689 deletions
47
build.zig
47
build.zig
|
|
@ -663,11 +663,10 @@ fn buildExamples(
|
||||||
deps: []const Dependency = &.{},
|
deps: []const Dependency = &.{},
|
||||||
std_platform_only: bool = false,
|
std_platform_only: bool = false,
|
||||||
has_assets: bool = false,
|
has_assets: bool = false,
|
||||||
use_module_api: bool = false,
|
|
||||||
}{
|
}{
|
||||||
.{ .name = "sysaudio", .deps = &.{}, .use_module_api = true },
|
.{ .name = "sysaudio", .deps = &.{} },
|
||||||
.{ .name = "core-custom-entrypoint", .deps = &.{}, .use_module_api = true },
|
.{ .name = "core-custom-entrypoint", .deps = &.{} },
|
||||||
.{ .name = "custom-renderer", .deps = &.{}, .use_module_api = true },
|
.{ .name = "custom-renderer", .deps = &.{} },
|
||||||
.{
|
.{
|
||||||
.name = "sprite",
|
.name = "sprite",
|
||||||
.deps = &.{ .zigimg, .assets },
|
.deps = &.{ .zigimg, .assets },
|
||||||
|
|
@ -691,7 +690,6 @@ fn buildExamples(
|
||||||
if (target.result.cpu.arch == .wasm32)
|
if (target.result.cpu.arch == .wasm32)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
if (example.use_module_api) {
|
|
||||||
const exe = b.addExecutable(.{
|
const exe = b.addExecutable(.{
|
||||||
.name = example.name,
|
.name = example.name,
|
||||||
.root_source_file = .{ .path = "examples/" ++ example.name ++ "/main.zig" },
|
.root_source_file = .{ .path = "examples/" ++ example.name ++ "/main.zig" },
|
||||||
|
|
@ -703,6 +701,11 @@ fn buildExamples(
|
||||||
link(b, exe, &exe.root_module);
|
link(b, exe, &exe.root_module);
|
||||||
b.installArtifact(exe);
|
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);
|
const compile_step = b.step(example.name, "Compile " ++ example.name);
|
||||||
compile_step.dependOn(b.getInstallStep());
|
compile_step.dependOn(b.getInstallStep());
|
||||||
|
|
||||||
|
|
@ -712,40 +715,6 @@ fn buildExamples(
|
||||||
|
|
||||||
const run_step = b.step("run-" ++ example.name, "Run " ++ example.name);
|
const run_step = b.step("run-" ++ example.name, "Run " ++ example.name);
|
||||||
run_step.dependOn(&run_cmd.step);
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
// TODO(important): review all code in this file in-depth
|
// TODO(important): review all code in this file in-depth
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const mach = @import("mach");
|
const mach = @import("mach");
|
||||||
const core = mach.core;
|
|
||||||
const gpu = mach.gpu;
|
const gpu = mach.gpu;
|
||||||
const gfx = mach.gfx;
|
const gfx = mach.gfx;
|
||||||
const math = mach.math;
|
const math = mach.math;
|
||||||
|
|
@ -25,19 +24,11 @@ sprites: usize,
|
||||||
rand: std.rand.DefaultPrng,
|
rand: std.rand.DefaultPrng,
|
||||||
time: f32,
|
time: f32,
|
||||||
pipeline: mach.EntityID,
|
pipeline: mach.EntityID,
|
||||||
|
frame_encoder: *gpu.CommandEncoder = undefined,
|
||||||
|
frame_render_pass: *gpu.RenderPassEncoder = undefined,
|
||||||
|
|
||||||
const d0 = 0.000001;
|
// 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.
|
||||||
// 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 name = .game;
|
||||||
pub const Mod = mach.Mod(@This());
|
pub const Mod = mach.Mod(@This());
|
||||||
|
|
||||||
|
|
@ -48,6 +39,7 @@ pub const global_events = .{
|
||||||
|
|
||||||
pub const local_events = .{
|
pub const local_events = .{
|
||||||
.after_init = .{ .handler = afterInit },
|
.after_init = .{ .handler = afterInit },
|
||||||
|
.end_frame = .{ .handler = endFrame },
|
||||||
};
|
};
|
||||||
|
|
||||||
fn init(
|
fn init(
|
||||||
|
|
@ -69,7 +61,7 @@ fn afterInit(
|
||||||
game: *Mod,
|
game: *Mod,
|
||||||
) !void {
|
) !void {
|
||||||
// The Mach .core is where we set window options, etc.
|
// 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
|
// Create a sprite rendering pipeline
|
||||||
const texture = glyphs.state().texture;
|
const texture = glyphs.state().texture;
|
||||||
|
|
@ -103,14 +95,14 @@ fn afterInit(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tick(
|
fn tick(
|
||||||
engine: *mach.Engine.Mod,
|
core: *mach.Core.Mod,
|
||||||
sprite: *gfx.Sprite.Mod,
|
sprite: *gfx.Sprite.Mod,
|
||||||
sprite_pipeline: *gfx.SpritePipeline.Mod,
|
sprite_pipeline: *gfx.SpritePipeline.Mod,
|
||||||
glyphs: *Glyphs.Mod,
|
glyphs: *Glyphs.Mod,
|
||||||
game: *Mod,
|
game: *Mod,
|
||||||
) !void {
|
) !void {
|
||||||
// TODO(engine): event polling should occur in mach.Engine module and get fired as ECS events.
|
// TODO(important): event polling should occur in mach.Core module and get fired as ECS events.
|
||||||
var iter = core.pollEvents();
|
var iter = mach.core.pollEvents();
|
||||||
var direction = game.state().direction;
|
var direction = game.state().direction;
|
||||||
var spawning = game.state().spawning;
|
var spawning = game.state().spawning;
|
||||||
while (iter.next()) |event| {
|
while (iter.next()) |event| {
|
||||||
|
|
@ -135,7 +127,7 @@ fn tick(
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
.close => engine.send(.exit, .{}),
|
.close => core.send(.exit, .{}),
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -155,7 +147,7 @@ fn tick(
|
||||||
const rand_index = game.state().rand.random().intRangeAtMost(usize, 0, glyphs.state().regions.count() - 1);
|
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 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, .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, .size, vec2(@floatFromInt(r.width), @floatFromInt(r.height)));
|
||||||
try sprite.set(new_entity, .uv_transform, Mat3x3.translate(vec2(@floatFromInt(r.x), @floatFromInt(r.y))));
|
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();
|
const delta_time = game.state().timer.lap();
|
||||||
|
|
||||||
// Animate entities
|
// Animate entities
|
||||||
var archetypes_iter = engine.entities.query(.{ .all = &.{
|
var archetypes_iter = core.entities.query(.{ .all = &.{
|
||||||
.{ .mach_gfx_sprite = &.{.transform} },
|
.{ .mach_gfx_sprite = &.{.transform} },
|
||||||
} });
|
} });
|
||||||
while (archetypes_iter.next()) |archetype| {
|
while (archetypes_iter.next()) |archetype| {
|
||||||
|
|
@ -176,8 +168,9 @@ fn tick(
|
||||||
const transforms = archetype.slice(.mach_gfx_sprite, .transform);
|
const transforms = archetype.slice(.mach_gfx_sprite, .transform);
|
||||||
for (ids, transforms) |id, *old_transform| {
|
for (ids, transforms) |id, *old_transform| {
|
||||||
var location = old_transform.translation();
|
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) {
|
// TODO: formatting
|
||||||
try engine.entities.remove(id);
|
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;
|
game.state().sprites -= 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -208,18 +201,53 @@ fn tick(
|
||||||
// Perform pre-render work
|
// Perform pre-render work
|
||||||
sprite_pipeline.send(.pre_render, .{});
|
sprite_pipeline.send(.pre_render, .{});
|
||||||
|
|
||||||
// Render a frame
|
// Create a command encoder for this frame
|
||||||
engine.send(.begin_pass, .{gpu.Color{ .r = 1.0, .g = 1.0, .b = 1.0, .a = 1.0 }});
|
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, .{});
|
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
|
// Every second, update the window title with the FPS
|
||||||
if (game.state().fps_timer.read() >= 1.0) {
|
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().fps_timer.reset();
|
||||||
game.state().frame_count = 0;
|
game.state().frame_count = 0;
|
||||||
}
|
}
|
||||||
game.state().frame_count += 1;
|
game.state().frame_count += 1;
|
||||||
game.state().time += delta_time;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,10 +38,10 @@ fn deinit(glyphs: *Mod) !void {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init(
|
fn init(
|
||||||
engine: *mach.Engine.Mod,
|
core: *mach.Core.Mod,
|
||||||
glyphs: *Mod,
|
glyphs: *Mod,
|
||||||
) !void {
|
) !void {
|
||||||
const device = engine.state().device;
|
const device = core.state().device;
|
||||||
const allocator = gpa.allocator();
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
// rgba32_pixels
|
// rgba32_pixels
|
||||||
|
|
@ -77,11 +77,11 @@ fn init(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare(
|
fn prepare(
|
||||||
engine: *mach.Engine.Mod,
|
core: *mach.Core.Mod,
|
||||||
glyphs: *Mod,
|
glyphs: *Mod,
|
||||||
codepoints: []const u21,
|
codepoints: []const u21,
|
||||||
) !void {
|
) !void {
|
||||||
const device = engine.state().device;
|
const device = core.state().device;
|
||||||
const queue = device.getQueue();
|
const queue = device.getQueue();
|
||||||
var s = glyphs.state();
|
var s = glyphs.state();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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");
|
const mach = @import("mach");
|
||||||
|
|
||||||
// The list of modules to be used in our application. Our game itself is implemented in our own
|
// The global list of Mach modules registered for use in our application.
|
||||||
// module called Game.
|
|
||||||
pub const modules = .{
|
pub const modules = .{
|
||||||
mach.Engine,
|
mach.Core,
|
||||||
mach.gfx.Sprite,
|
mach.gfx.Sprite,
|
||||||
mach.gfx.SpritePipeline,
|
mach.gfx.SpritePipeline,
|
||||||
@import("Glyphs.zig"),
|
@import("Glyphs.zig"),
|
||||||
@import("Game.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()) {}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ const std = @import("std");
|
||||||
const zigimg = @import("zigimg");
|
const zigimg = @import("zigimg");
|
||||||
const assets = @import("assets");
|
const assets = @import("assets");
|
||||||
const mach = @import("mach");
|
const mach = @import("mach");
|
||||||
const core = mach.core;
|
|
||||||
const gpu = mach.gpu;
|
const gpu = mach.gpu;
|
||||||
const gfx = mach.gfx;
|
const gfx = mach.gfx;
|
||||||
const math = mach.math;
|
const math = mach.math;
|
||||||
|
|
@ -29,19 +28,11 @@ rand: std.rand.DefaultPrng,
|
||||||
time: f32,
|
time: f32,
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
pipeline: mach.EntityID,
|
pipeline: mach.EntityID,
|
||||||
|
frame_encoder: *gpu.CommandEncoder = undefined,
|
||||||
|
frame_render_pass: *gpu.RenderPassEncoder = undefined,
|
||||||
|
|
||||||
const d0 = 0.000001;
|
// 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.
|
||||||
// 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 name = .game;
|
||||||
pub const Mod = mach.Mod(@This());
|
pub const Mod = mach.Mod(@This());
|
||||||
|
|
||||||
|
|
@ -50,14 +41,18 @@ pub const global_events = .{
|
||||||
.tick = .{ .handler = tick },
|
.tick = .{ .handler = tick },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const local_events = .{
|
||||||
|
.end_frame = .{ .handler = endFrame },
|
||||||
|
};
|
||||||
|
|
||||||
fn init(
|
fn init(
|
||||||
engine: *mach.Engine.Mod,
|
core: *mach.Core.Mod,
|
||||||
sprite: *gfx.Sprite.Mod,
|
sprite: *gfx.Sprite.Mod,
|
||||||
sprite_pipeline: *gfx.SpritePipeline.Mod,
|
sprite_pipeline: *gfx.SpritePipeline.Mod,
|
||||||
game: *Mod,
|
game: *Mod,
|
||||||
) !void {
|
) !void {
|
||||||
// The Mach .core is where we set window options, etc.
|
// 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
|
// 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
|
// 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
|
// Create a sprite rendering pipeline
|
||||||
const allocator = gpa.allocator();
|
const allocator = gpa.allocator();
|
||||||
const pipeline = try engine.newEntity();
|
const pipeline = try core.newEntity();
|
||||||
try sprite_pipeline.set(pipeline, .texture, try loadTexture(engine, allocator));
|
try sprite_pipeline.set(pipeline, .texture, try loadTexture(core, allocator));
|
||||||
sprite_pipeline.send(.update, .{});
|
sprite_pipeline.send(.update, .{});
|
||||||
|
|
||||||
// Create our player sprite
|
// 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, .transform, Mat4x4.translate(vec3(-0.02, 0, 0)));
|
||||||
try sprite.set(player, .size, vec2(32, 32));
|
try sprite.set(player, .size, vec2(32, 32));
|
||||||
try sprite.set(player, .uv_transform, Mat3x3.translate(vec2(0, 0)));
|
try sprite.set(player, .uv_transform, Mat3x3.translate(vec2(0, 0)));
|
||||||
|
|
@ -92,13 +87,13 @@ fn init(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tick(
|
fn tick(
|
||||||
engine: *mach.Engine.Mod,
|
core: *mach.Core.Mod,
|
||||||
sprite: *gfx.Sprite.Mod,
|
sprite: *gfx.Sprite.Mod,
|
||||||
sprite_pipeline: *gfx.SpritePipeline.Mod,
|
sprite_pipeline: *gfx.SpritePipeline.Mod,
|
||||||
game: *Mod,
|
game: *Mod,
|
||||||
) !void {
|
) !void {
|
||||||
// TODO(engine): event polling should occur in mach.Engine module and get fired as ECS events.
|
// TODO(important): event polling should occur in mach.Core module and get fired as ECS events.
|
||||||
var iter = core.pollEvents();
|
var iter = mach.core.pollEvents();
|
||||||
var direction = game.state().direction;
|
var direction = game.state().direction;
|
||||||
var spawning = game.state().spawning;
|
var spawning = game.state().spawning;
|
||||||
while (iter.next()) |event| {
|
while (iter.next()) |event| {
|
||||||
|
|
@ -123,7 +118,7 @@ fn tick(
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
.close => engine.send(.exit, .{}),
|
.close => core.send(.exit, .{}),
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -140,7 +135,7 @@ fn tick(
|
||||||
new_pos.v[0] += game.state().rand.random().floatNorm(f32) * 25;
|
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[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, .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, .size, vec2(32, 32));
|
||||||
try sprite.set(new_entity, .uv_transform, Mat3x3.translate(vec2(0, 0)));
|
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();
|
const delta_time = game.state().timer.lap();
|
||||||
|
|
||||||
// Rotate entities
|
// Rotate entities
|
||||||
var archetypes_iter = engine.entities.query(.{ .all = &.{
|
var archetypes_iter = core.entities.query(.{ .all = &.{
|
||||||
.{ .mach_gfx_sprite = &.{.transform} },
|
.{ .mach_gfx_sprite = &.{.transform} },
|
||||||
} });
|
} });
|
||||||
while (archetypes_iter.next()) |archetype| {
|
while (archetypes_iter.next()) |archetype| {
|
||||||
|
|
@ -187,26 +182,60 @@ fn tick(
|
||||||
// Perform pre-render work
|
// Perform pre-render work
|
||||||
sprite_pipeline.send(.pre_render, .{});
|
sprite_pipeline.send(.pre_render, .{});
|
||||||
|
|
||||||
// Render a frame
|
// Create a command encoder for this frame
|
||||||
engine.send(.begin_pass, .{gpu.Color{ .r = 1.0, .g = 1.0, .b = 1.0, .a = 1.0 }});
|
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, .{});
|
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
|
// Every second, update the window title with the FPS
|
||||||
if (game.state().fps_timer.read() >= 1.0) {
|
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().fps_timer.reset();
|
||||||
game.state().frame_count = 0;
|
game.state().frame_count = 0;
|
||||||
}
|
}
|
||||||
game.state().frame_count += 1;
|
game.state().frame_count += 1;
|
||||||
game.state().time += delta_time;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: move this helper into gfx module
|
// TODO: move this helper into gfx module
|
||||||
fn loadTexture(engine: *mach.Engine.Mod, allocator: std.mem.Allocator) !*gpu.Texture {
|
fn loadTexture(core: *mach.Core.Mod, allocator: std.mem.Allocator) !*gpu.Texture {
|
||||||
const device = engine.state().device;
|
const device = core.state().device;
|
||||||
const queue = device.getQueue();
|
const queue = device.getQueue();
|
||||||
|
|
||||||
// Load the image from memory
|
// Load the image from memory
|
||||||
|
|
|
||||||
|
|
@ -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 mach = @import("mach");
|
||||||
|
|
||||||
const Game = @import("Game.zig");
|
// The global list of Mach modules registered for use in our application.
|
||||||
|
|
||||||
// The list of modules to be used in our application. Our game itself is implemented in our own
|
|
||||||
// module called Game.
|
|
||||||
pub const modules = .{
|
pub const modules = .{
|
||||||
mach.Engine,
|
mach.Core,
|
||||||
mach.gfx.Sprite,
|
mach.gfx.Sprite,
|
||||||
mach.gfx.SpritePipeline,
|
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()) {}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,8 @@ const std = @import("std");
|
||||||
const zigimg = @import("zigimg");
|
const zigimg = @import("zigimg");
|
||||||
const assets = @import("assets");
|
const assets = @import("assets");
|
||||||
const mach = @import("mach");
|
const mach = @import("mach");
|
||||||
const core = mach.core;
|
|
||||||
const gfx = mach.gfx;
|
const gfx = mach.gfx;
|
||||||
const gpu = mach.gpu;
|
const gpu = mach.gpu;
|
||||||
const Text = mach.gfx.Text;
|
|
||||||
const math = mach.math;
|
const math = mach.math;
|
||||||
|
|
||||||
const vec2 = math.vec2;
|
const vec2 = math.vec2;
|
||||||
|
|
@ -26,35 +24,26 @@ spawning: bool = false,
|
||||||
spawn_timer: mach.Timer,
|
spawn_timer: mach.Timer,
|
||||||
fps_timer: mach.Timer,
|
fps_timer: mach.Timer,
|
||||||
frame_count: usize,
|
frame_count: usize,
|
||||||
texts: usize,
|
|
||||||
rand: std.rand.DefaultPrng,
|
rand: std.rand.DefaultPrng,
|
||||||
time: f32,
|
time: f32,
|
||||||
style1: mach.EntityID,
|
style1: mach.EntityID,
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
|
pipeline: mach.EntityID,
|
||||||
|
frame_encoder: *gpu.CommandEncoder = undefined,
|
||||||
|
frame_render_pass: *gpu.RenderPassEncoder = undefined,
|
||||||
|
|
||||||
const d0 = 0.000001;
|
// 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.
|
||||||
// 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 name = .game;
|
||||||
pub const Mod = mach.Mod(@This());
|
pub const Mod = mach.Mod(@This());
|
||||||
|
|
||||||
pub const global_events = .{
|
pub const global_events = .{
|
||||||
.init = .{ .handler = init },
|
.init = .{ .handler = init },
|
||||||
.deinit = .{ .handler = deinit },
|
|
||||||
.tick = .{ .handler = tick },
|
.tick = .{ .handler = tick },
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Pipeline = enum(u32) {
|
pub const local_events = .{
|
||||||
default,
|
.end_frame = .{ .handler = endFrame },
|
||||||
};
|
};
|
||||||
|
|
||||||
const upscale = 1.0;
|
const upscale = 1.0;
|
||||||
|
|
@ -65,42 +54,49 @@ const text1: []const []const u8 = &.{
|
||||||
"bold\nand\n",
|
"bold\nand\n",
|
||||||
};
|
};
|
||||||
|
|
||||||
const text2: []const []const u8 = &.{"!$?😊"};
|
const text2: []const []const u8 = &.{"$!?😊"};
|
||||||
|
|
||||||
fn init(
|
fn init(
|
||||||
engine: *mach.Engine.Mod,
|
core: *mach.Core.Mod,
|
||||||
text: *Text.Mod,
|
text: *gfx.Text.Mod,
|
||||||
|
text_pipeline: *gfx.TextPipeline.Mod,
|
||||||
text_style: *gfx.TextStyle.Mod,
|
text_style: *gfx.TextStyle.Mod,
|
||||||
game: *Mod,
|
game: *Mod,
|
||||||
) !void {
|
) !void {
|
||||||
// The Mach .core is where we set window options, etc.
|
// 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
|
// 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_name, "Roboto Medium"); // TODO
|
||||||
try text_style.set(style1, .font_size, 48 * gfx.px_per_pt); // 48pt
|
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, .font_weight, gfx.font_weight_normal);
|
||||||
try text_style.set(style1, .italic, false);
|
try text_style.set(style1, .italic, false);
|
||||||
try text_style.set(style1, .color, vec4(0.6, 1.0, 0.6, 1.0));
|
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_name, "Roboto Medium"); // TODO
|
||||||
try text_style.set(style2, .font_size, 48 * gfx.px_per_pt); // 48pt
|
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, .font_weight, gfx.font_weight_normal);
|
||||||
try text_style.set(style2, .italic, true);
|
try text_style.set(style2, .italic, true);
|
||||||
try text_style.set(style2, .color, vec4(0.6, 1.0, 0.6, 1.0));
|
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_name, "Roboto Medium"); // TODO
|
||||||
try text_style.set(style3, .font_size, 48 * gfx.px_per_pt); // 48pt
|
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, .font_weight, gfx.font_weight_bold);
|
||||||
try text_style.set(style3, .italic, false);
|
try text_style.set(style3, .italic, false);
|
||||||
try text_style.set(style3, .color, vec4(0.6, 1.0, 0.6, 1.0));
|
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
|
// Create some text
|
||||||
const player = try engine.newEntity();
|
const player = try core.newEntity();
|
||||||
try text.set(player, .pipeline, @intFromEnum(Pipeline.default));
|
try text.set(player, .pipeline, pipeline);
|
||||||
try text.set(player, .transform, Mat4x4.scaleScalar(upscale).mul(&Mat4x4.translate(vec3(0, 0, 0))));
|
try text.set(player, .transform, Mat4x4.scaleScalar(upscale).mul(&Mat4x4.translate(vec3(0, 0, 0))));
|
||||||
|
|
||||||
// TODO: better storage mechanism for this
|
// TODO: better storage mechanism for this
|
||||||
|
|
@ -112,10 +108,7 @@ fn init(
|
||||||
styles[2] = style3;
|
styles[2] = style3;
|
||||||
try text.set(player, .text, text1);
|
try text.set(player, .text, text1);
|
||||||
try text.set(player, .style, styles);
|
try text.set(player, .style, styles);
|
||||||
|
try text.set(player, .dirty, true);
|
||||||
text.send(.init_pipeline, .{Text.PipelineOptions{
|
|
||||||
.pipeline = @intFromEnum(Pipeline.default),
|
|
||||||
}});
|
|
||||||
|
|
||||||
game.init(.{
|
game.init(.{
|
||||||
.timer = try mach.Timer.start(),
|
.timer = try mach.Timer.start(),
|
||||||
|
|
@ -123,25 +116,22 @@ fn init(
|
||||||
.player = player,
|
.player = player,
|
||||||
.fps_timer = try mach.Timer.start(),
|
.fps_timer = try mach.Timer.start(),
|
||||||
.frame_count = 0,
|
.frame_count = 0,
|
||||||
.texts = 0,
|
|
||||||
.rand = std.rand.DefaultPrng.init(1337),
|
.rand = std.rand.DefaultPrng.init(1337),
|
||||||
.time = 0,
|
.time = 0,
|
||||||
.style1 = style1,
|
.style1 = style1,
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
|
.pipeline = pipeline,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deinit(engine: *mach.Engine.Mod) !void {
|
|
||||||
_ = engine;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn tick(
|
fn tick(
|
||||||
engine: *mach.Engine.Mod,
|
core: *mach.Core.Mod,
|
||||||
text: *Text.Mod,
|
text: *gfx.Text.Mod,
|
||||||
|
text_pipeline: *gfx.TextPipeline.Mod,
|
||||||
game: *Mod,
|
game: *Mod,
|
||||||
) !void {
|
) !void {
|
||||||
// TODO(engine): event polling should occur in mach.Engine module and get fired as ECS events.
|
// TODO(important): event polling should occur in mach.Core module and get fired as ECS events.
|
||||||
var iter = core.pollEvents();
|
var iter = mach.core.pollEvents();
|
||||||
var direction = game.state().direction;
|
var direction = game.state().direction;
|
||||||
var spawning = game.state().spawning;
|
var spawning = game.state().spawning;
|
||||||
while (iter.next()) |event| {
|
while (iter.next()) |event| {
|
||||||
|
|
@ -166,7 +156,7 @@ fn tick(
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
.close => engine.send(.exit, .{}),
|
.close => core.send(.exit, .{}),
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -175,16 +165,16 @@ fn tick(
|
||||||
|
|
||||||
var player_transform = text.get(game.state().player, .transform).?;
|
var player_transform = text.get(game.state().player, .transform).?;
|
||||||
var player_pos = player_transform.translation().divScalar(upscale);
|
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
|
// Spawn new entities
|
||||||
_ = game.state().spawn_timer.lap();
|
_ = game.state().spawn_timer.lap();
|
||||||
for (0..1) |_| {
|
for (0..10) |_| {
|
||||||
var new_pos = player_pos;
|
var new_pos = player_pos;
|
||||||
new_pos.v[0] += 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) * 25;
|
new_pos.v[1] += game.state().rand.random().floatNorm(f32) * 50;
|
||||||
|
|
||||||
const new_entity = try engine.newEntity();
|
const new_entity = try core.newEntity();
|
||||||
try text.set(new_entity, .pipeline, @intFromEnum(Pipeline.default));
|
try text.set(new_entity, .pipeline, game.state().pipeline);
|
||||||
try text.set(new_entity, .transform, Mat4x4.scaleScalar(upscale).mul(&Mat4x4.translate(new_pos)));
|
try text.set(new_entity, .transform, Mat4x4.scaleScalar(upscale).mul(&Mat4x4.translate(new_pos)));
|
||||||
|
|
||||||
// TODO: better storage mechanism for this
|
// TODO: better storage mechanism for this
|
||||||
|
|
@ -193,8 +183,7 @@ fn tick(
|
||||||
styles[0] = game.state().style1;
|
styles[0] = game.state().style1;
|
||||||
try text.set(new_entity, .text, text2);
|
try text.set(new_entity, .text, text2);
|
||||||
try text.set(new_entity, .style, styles);
|
try text.set(new_entity, .style, styles);
|
||||||
|
try text.set(new_entity, .dirty, true);
|
||||||
game.state().texts += 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -202,7 +191,7 @@ fn tick(
|
||||||
const delta_time = game.state().timer.lap();
|
const delta_time = game.state().timer.lap();
|
||||||
|
|
||||||
// Rotate entities
|
// Rotate entities
|
||||||
var archetypes_iter = engine.entities.query(.{ .all = &.{
|
var archetypes_iter = core.entities.query(.{ .all = &.{
|
||||||
.{ .mach_gfx_text = &.{.transform} },
|
.{ .mach_gfx_text = &.{.transform} },
|
||||||
} });
|
} });
|
||||||
while (archetypes_iter.next()) |archetype| {
|
while (archetypes_iter.next()) |archetype| {
|
||||||
|
|
@ -231,23 +220,75 @@ fn tick(
|
||||||
player_pos.v[0] += direction.x() * speed * delta_time;
|
player_pos.v[0] += direction.x() * speed * delta_time;
|
||||||
player_pos.v[1] += direction.y() * 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)));
|
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
|
// Perform pre-render work
|
||||||
text.send(.pre_render, .{@intFromEnum(Pipeline.default)});
|
text_pipeline.send(.pre_render, .{});
|
||||||
|
|
||||||
// Render a frame
|
// Create a command encoder for this frame
|
||||||
engine.send(.begin_pass, .{gpu.Color{ .r = 1.0, .g = 1.0, .b = 1.0, .a = 1.0 }});
|
game.state().frame_encoder = mach.core.device.createCommandEncoder(null);
|
||||||
text.send(.render, .{@intFromEnum(Pipeline.default)});
|
|
||||||
engine.send(.end_pass, .{});
|
// Grab the back buffer of the swapchain
|
||||||
engine.send(.frame_done, .{}); // Present the frame
|
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
|
// Every second, update the window title with the FPS
|
||||||
if (game.state().fps_timer.read() >= 1.0) {
|
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().fps_timer.reset();
|
||||||
game.state().frame_count = 0;
|
game.state().frame_count = 0;
|
||||||
}
|
}
|
||||||
game.state().frame_count += 1;
|
game.state().frame_count += 1;
|
||||||
game.state().time += delta_time;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 mach = @import("mach");
|
||||||
|
|
||||||
const Game = @import("Game.zig");
|
// The global list of Mach modules registered for use in our application.
|
||||||
|
|
||||||
// The list of modules to be used in our application. Our game itself is implemented in our own
|
|
||||||
// module called Game.
|
|
||||||
pub const modules = .{
|
pub const modules = .{
|
||||||
mach.Engine,
|
mach.Core,
|
||||||
mach.gfx.Text,
|
mach.gfx.Text,
|
||||||
|
mach.gfx.TextPipeline,
|
||||||
mach.gfx.TextStyle,
|
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()) {}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const mach = @import("../main.zig");
|
const mach = @import("../main.zig");
|
||||||
const core = mach.core;
|
const gpu = mach.gpu;
|
||||||
const gpu = mach.core.gpu;
|
|
||||||
const gfx = mach.gfx;
|
const gfx = mach.gfx;
|
||||||
const Engine = mach.Engine;
|
const Engine = mach.Engine;
|
||||||
|
|
||||||
|
|
@ -44,7 +43,7 @@ pub const local_events = .{
|
||||||
.update = .{ .handler = update },
|
.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 = &.{
|
var archetypes_iter = sprite_pipeline.entities.query(.{ .all = &.{
|
||||||
.{ .mach_gfx_sprite_pipeline = &.{
|
.{ .mach_gfx_sprite_pipeline = &.{
|
||||||
.built,
|
.built,
|
||||||
|
|
@ -54,19 +53,19 @@ fn update(engine: *Engine.Mod, sprite: *Mod, sprite_pipeline: *gfx.SpritePipelin
|
||||||
const ids = archetype.slice(.entity, .id);
|
const ids = archetype.slice(.entity, .id);
|
||||||
const built_pipelines = archetype.slice(.mach_gfx_sprite_pipeline, .built);
|
const built_pipelines = archetype.slice(.mach_gfx_sprite_pipeline, .built);
|
||||||
for (ids, built_pipelines) |pipeline_id, *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(
|
fn updatePipeline(
|
||||||
engine: *Engine.Mod,
|
core: *mach.Core.Mod,
|
||||||
sprite: *Mod,
|
sprite: *Mod,
|
||||||
sprite_pipeline: *gfx.SpritePipeline.Mod,
|
sprite_pipeline: *gfx.SpritePipeline.Mod,
|
||||||
pipeline_id: mach.EntityID,
|
pipeline_id: mach.EntityID,
|
||||||
built: *gfx.SpritePipeline.BuiltPipeline,
|
built: *gfx.SpritePipeline.BuiltPipeline,
|
||||||
) !void {
|
) !void {
|
||||||
const device = engine.state().device;
|
const device = core.state().device;
|
||||||
const encoder = device.createCommandEncoder(null);
|
const encoder = device.createCommandEncoder(null);
|
||||||
defer encoder.release();
|
defer encoder.release();
|
||||||
|
|
||||||
|
|
@ -110,6 +109,6 @@ fn updatePipeline(
|
||||||
encoder.writeBuffer(built.sizes, 0, gfx.SpritePipeline.cp_sizes[0..i]);
|
encoder.writeBuffer(built.sizes, 0, gfx.SpritePipeline.cp_sizes[0..i]);
|
||||||
var command = encoder.finish(null);
|
var command = encoder.finish(null);
|
||||||
defer command.release();
|
defer command.release();
|
||||||
engine.state().queue.submit(&[_]*gpu.CommandBuffer{command});
|
core.state().queue.submit(&[_]*gpu.CommandBuffer{command});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const mach = @import("../main.zig");
|
const mach = @import("../main.zig");
|
||||||
const core = mach.core;
|
|
||||||
|
|
||||||
const gpu = mach.gpu;
|
const gpu = mach.gpu;
|
||||||
const math = mach.math;
|
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_uv_transforms: [sprite_buffer_cap]math.Mat3x3 = undefined;
|
||||||
pub var cp_sizes: [sprite_buffer_cap]math.Vec2 = 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 {
|
pub const BuiltPipeline = struct {
|
||||||
render: *gpu.RenderPipeline,
|
render: *gpu.RenderPipeline,
|
||||||
texture_sampler: *gpu.Sampler,
|
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.
|
// Destroy all sprite render pipelines. We will rebuild them all.
|
||||||
deinit(sprite_pipeline);
|
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);
|
const textures = archetype.slice(.mach_gfx_sprite_pipeline, .texture);
|
||||||
|
|
||||||
for (ids, textures) |pipeline_id, 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(
|
fn buildPipeline(
|
||||||
engine: *mach.Engine.Mod,
|
core: *mach.Core.Mod,
|
||||||
sprite_pipeline: *Mod,
|
sprite_pipeline: *Mod,
|
||||||
pipeline_id: mach.EntityID,
|
pipeline_id: mach.EntityID,
|
||||||
texture: *gpu.Texture,
|
texture: *gpu.Texture,
|
||||||
|
|
@ -181,7 +185,7 @@ fn buildPipeline(
|
||||||
const opt_fragment_state = sprite_pipeline.get(pipeline_id, .fragment_state);
|
const opt_fragment_state = sprite_pipeline.get(pipeline_id, .fragment_state);
|
||||||
const opt_layout = sprite_pipeline.get(pipeline_id, .layout);
|
const opt_layout = sprite_pipeline.get(pipeline_id, .layout);
|
||||||
|
|
||||||
const device = engine.state().device;
|
const device = core.state().device;
|
||||||
|
|
||||||
// Storage buffers
|
// Storage buffers
|
||||||
const transforms = device.createBuffer(&.{
|
const transforms = device.createBuffer(&.{
|
||||||
|
|
@ -269,7 +273,7 @@ fn buildPipeline(
|
||||||
defer shader_module.release();
|
defer shader_module.release();
|
||||||
|
|
||||||
const color_target = opt_color_target_state orelse gpu.ColorTargetState{
|
const color_target = opt_color_target_state orelse gpu.ColorTargetState{
|
||||||
.format = core.descriptor.format,
|
.format = mach.core.descriptor.format,
|
||||||
.blend = &blend_state,
|
.blend = &blend_state,
|
||||||
.write_mask = gpu.ColorWriteMaskFlags.all,
|
.write_mask = gpu.ColorWriteMaskFlags.all,
|
||||||
};
|
};
|
||||||
|
|
@ -311,10 +315,12 @@ fn buildPipeline(
|
||||||
try sprite_pipeline.set(pipeline_id, .num_sprites, 0);
|
try sprite_pipeline.set(pipeline_id, .num_sprites, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn preRender(
|
fn preRender(sprite_pipeline: *Mod) void {
|
||||||
engine: *mach.Engine.Mod,
|
const encoder = mach.core.device.createCommandEncoder(&gpu.CommandEncoder.Descriptor{
|
||||||
sprite_pipeline: *Mod,
|
.label = "SpritePipeline.encoder",
|
||||||
) void {
|
});
|
||||||
|
defer encoder.release();
|
||||||
|
|
||||||
var archetypes_iter = sprite_pipeline.entities.query(.{ .all = &.{
|
var archetypes_iter = sprite_pipeline.entities.query(.{ .all = &.{
|
||||||
.{ .mach_gfx_sprite_pipeline = &.{
|
.{ .mach_gfx_sprite_pipeline = &.{
|
||||||
.built,
|
.built,
|
||||||
|
|
@ -326,10 +332,10 @@ fn preRender(
|
||||||
// Create the projection matrix
|
// Create the projection matrix
|
||||||
// TODO(sprite): move this out of the hot codepath
|
// TODO(sprite): move this out of the hot codepath
|
||||||
const proj = math.Mat4x4.projection2D(.{
|
const proj = math.Mat4x4.projection2D(.{
|
||||||
.left = -@as(f32, @floatFromInt(core.size().width)) / 2,
|
.left = -@as(f32, @floatFromInt(mach.core.size().width)) / 2,
|
||||||
.right = @as(f32, @floatFromInt(core.size().width)) / 2,
|
.right = @as(f32, @floatFromInt(mach.core.size().width)) / 2,
|
||||||
.bottom = -@as(f32, @floatFromInt(core.size().height)) / 2,
|
.bottom = -@as(f32, @floatFromInt(mach.core.size().height)) / 2,
|
||||||
.top = @as(f32, @floatFromInt(core.size().height)) / 2,
|
.top = @as(f32, @floatFromInt(mach.core.size().height)) / 2,
|
||||||
.near = -0.1,
|
.near = -0.1,
|
||||||
.far = 100000,
|
.far = 100000,
|
||||||
});
|
});
|
||||||
|
|
@ -343,15 +349,20 @@ fn preRender(
|
||||||
@as(f32, @floatFromInt(built.texture.getHeight())),
|
@as(f32, @floatFromInt(built.texture.getHeight())),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
engine.state().encoder.writeBuffer(built.uniforms, 0, &[_]Uniforms{uniforms});
|
encoder.writeBuffer(built.uniforms, 0, &[_]Uniforms{uniforms});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render(
|
var command = encoder.finish(null);
|
||||||
engine: *mach.Engine.Mod,
|
defer command.release();
|
||||||
sprite_pipeline: *Mod,
|
mach.core.queue.submit(&[_]*gpu.CommandBuffer{command});
|
||||||
) !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 = &.{
|
var archetypes_iter = sprite_pipeline.entities.query(.{ .all = &.{
|
||||||
.{ .mach_gfx_sprite_pipeline = &.{
|
.{ .mach_gfx_sprite_pipeline = &.{
|
||||||
.built,
|
.built,
|
||||||
|
|
@ -362,12 +373,11 @@ fn render(
|
||||||
const built_pipelines = archetype.slice(.mach_gfx_sprite_pipeline, .built);
|
const built_pipelines = archetype.slice(.mach_gfx_sprite_pipeline, .built);
|
||||||
for (ids, built_pipelines) |pipeline_id, built| {
|
for (ids, built_pipelines) |pipeline_id, built| {
|
||||||
// Draw the sprite batch
|
// Draw the sprite batch
|
||||||
const pass = engine.state().pass;
|
|
||||||
const total_vertices = sprite_pipeline.get(pipeline_id, .num_sprites).? * 6;
|
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?
|
// TODO(sprite): remove dynamic offsets?
|
||||||
pass.setBindGroup(0, built.bind_group, &.{});
|
render_pass.setBindGroup(0, built.bind_group, &.{});
|
||||||
pass.draw(total_vertices, 1, 0, 0);
|
render_pass.draw(total_vertices, 1, 0, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
559
src/gfx/Text.zig
559
src/gfx/Text.zig
|
|
@ -1,6 +1,5 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const mach = @import("../main.zig");
|
const mach = @import("../main.zig");
|
||||||
const core = mach.core;
|
|
||||||
const gpu = mach.gpu;
|
const gpu = mach.gpu;
|
||||||
const Engine = mach.Engine;
|
const Engine = mach.Engine;
|
||||||
const gfx = mach.gfx;
|
const gfx = mach.gfx;
|
||||||
|
|
@ -14,22 +13,10 @@ const vec4 = math.vec4;
|
||||||
const Mat3x3 = math.Mat3x3;
|
const Mat3x3 = math.Mat3x3;
|
||||||
const Mat4x4 = math.Mat4x4;
|
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 name = .mach_gfx_text;
|
||||||
pub const Mod = mach.Mod(@This());
|
pub const Mod = mach.Mod(@This());
|
||||||
|
|
||||||
pub const components = .{
|
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 =
|
.transform = .{ .type = Mat4x4, .description =
|
||||||
\\ The text model transformation matrix. Text is measured in pixel units, starting from
|
\\ 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
|
\\ (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.
|
\\ Expected to match the length of the text component.
|
||||||
},
|
},
|
||||||
};
|
|
||||||
|
|
||||||
pub const global_events = .{
|
.dirty = .{ .type = bool, .description =
|
||||||
.deinit = .{ .handler = deinit },
|
\\ If true, the underlying glyph buffers, texture atlas, and transform buffers will be updated
|
||||||
.init = .{ .handler = init },
|
\\ 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 = .{
|
pub const local_events = .{
|
||||||
.init_pipeline = .{ .handler = initPipeline },
|
.update = .{ .handler = update },
|
||||||
.updated = .{ .handler = updated },
|
|
||||||
.pre_render = .{ .handler = preRender },
|
|
||||||
.render = .{ .handler = render },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const Uniforms = extern struct {
|
const BuiltText = struct {
|
||||||
// WebGPU requires that the size of struct fields are multiples of 16
|
glyphs: std.ArrayListUnmanaged(gfx.TextPipeline.Glyph),
|
||||||
// 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 Glyph = extern struct {
|
fn update(core: *mach.Core.Mod, text: *Mod, text_pipeline: *gfx.TextPipeline.Mod) !void {
|
||||||
/// Position of this glyph (top-left corner.)
|
var archetypes_iter = text_pipeline.entities.query(.{ .all = &.{
|
||||||
pos: Vec2,
|
.{ .mach_gfx_text_pipeline = &.{
|
||||||
|
.built,
|
||||||
/// 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,
|
|
||||||
} },
|
} },
|
||||||
} });
|
} });
|
||||||
|
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);
|
const encoder = device.createCommandEncoder(null);
|
||||||
defer encoder.release();
|
defer encoder.release();
|
||||||
|
|
||||||
pipeline.num_texts = 0;
|
const allocator = text_pipeline.state().allocator;
|
||||||
pipeline.num_glyphs = 0;
|
var glyphs = if (text_pipeline.state().glyph_update_buffer) |*b| b else blk: {
|
||||||
var glyphs = std.ArrayListUnmanaged(Glyph){};
|
// TODO(text): better default allocation size
|
||||||
var transforms_offset: usize = 0;
|
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 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| {
|
while (archetypes_iter.next()) |archetype| {
|
||||||
|
const ids = archetype.slice(.entity, .id);
|
||||||
const transforms = archetype.slice(.mach_gfx_text, .transform);
|
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
|
// TODO: currently we cannot query all texts which have a _single_ pipeline component
|
||||||
// to live?
|
// value and get back contiguous memory for all of them. This is because all texts with
|
||||||
encoder.writeBuffer(pipeline.transforms, transforms_offset, transforms);
|
// possibly different pipeline component values are stored as the same archetype. If we
|
||||||
// encoder.writeBuffer(pipeline.colors, colors_offset, colors);
|
// 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;
|
gfx.TextPipeline.cp_transforms[num_texts] = transform;
|
||||||
// colors_offset += colors.len;
|
|
||||||
pipeline.num_texts += @intCast(transforms.len);
|
|
||||||
|
|
||||||
// Render texts
|
if (text.get(id, .dirty) == null) {
|
||||||
// TODO: this is very expensive and shouldn't be done here, should be done only on detected
|
// We do not need to rebuild this specific entity, so use cached glyph information
|
||||||
// text change.
|
// from its previous build.
|
||||||
const px_density = 2.0;
|
const built_text = text.get(id, .built).?;
|
||||||
const segment_lists = archetype.slice(.mach_gfx_text, .text);
|
for (built_text.glyphs.items) |*glyph| glyph.text_index = num_texts;
|
||||||
const style_lists = archetype.slice(.mach_gfx_text, .style);
|
try glyphs.appendSlice(allocator, built_text.glyphs.items);
|
||||||
for (segment_lists, style_lists) |segments, styles| {
|
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_x: f32 = 0.0;
|
||||||
var origin_y: f32 = 0.0;
|
var origin_y: f32 = 0.0;
|
||||||
|
|
||||||
for (segments, styles) |segment, style| {
|
for (segments, styles) |segment, style| {
|
||||||
// Load a font
|
// Load the font
|
||||||
const font_name = engine.entities.getComponent(style, .mach_gfx_text_style, .font_name).?;
|
// 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
|
_ = font_name; // TODO: actually use font name
|
||||||
const font_bytes = @import("font-assets").fira_sans_regular_ttf;
|
const font_bytes = @import("font-assets").fira_sans_regular_ttf;
|
||||||
var font = try gfx.Font.initBytes(font_bytes);
|
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).?;
|
// TODO(text): respect these style parameters
|
||||||
const font_weight = engine.entities.getComponent(style, .mach_gfx_text_style, .font_weight);
|
const font_size = core.entities.getComponent(style, .mach_gfx_text_style, .font_size).?;
|
||||||
const italic = engine.entities.getComponent(style, .mach_gfx_text_style, .italic);
|
const font_weight = core.entities.getComponent(style, .mach_gfx_text_style, .font_weight);
|
||||||
const color = engine.entities.getComponent(style, .mach_gfx_text_style, .color);
|
_ = font_weight; // autofix
|
||||||
// TODO: actually apply these
|
const italic = core.entities.getComponent(style, .mach_gfx_text_style, .italic);
|
||||||
_ = font_weight;
|
_ = italic; // autofix
|
||||||
_ = italic;
|
const color = core.entities.getComponent(style, .mach_gfx_text_style, .color);
|
||||||
_ = color;
|
_ = color; // autofix
|
||||||
|
|
||||||
// Create a text shaper
|
// Create a text shaper
|
||||||
var run = try gfx.TextRun.init();
|
var run = try gfx.TextRun.init();
|
||||||
run.font_size_px = font_size;
|
run.font_size_px = font_size;
|
||||||
run.px_density = 2; // TODO
|
run.px_density = px_density;
|
||||||
|
|
||||||
defer run.deinit();
|
defer run.deinit();
|
||||||
|
|
||||||
run.addText(segment);
|
run.addText(segment);
|
||||||
|
|
@ -419,18 +175,23 @@ fn updated(
|
||||||
while (run.next()) |glyph| {
|
while (run.next()) |glyph| {
|
||||||
const codepoint = segment[glyph.cluster];
|
const codepoint = segment[glyph.cluster];
|
||||||
// TODO: use flags(?) to detect newline, or at least something more reliable?
|
// TODO: use flags(?) to detect newline, or at least something more reliable?
|
||||||
if (codepoint != '\n') {
|
if (codepoint == '\n') {
|
||||||
const region = try pipeline.regions.getOrPut(text.state().allocator, .{
|
origin_x = 0;
|
||||||
|
origin_y -= font_size;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const region = try built.regions.getOrPut(allocator, .{
|
||||||
.index = glyph.glyph_index,
|
.index = glyph.glyph_index,
|
||||||
.size = @bitCast(font_size),
|
.size = @bitCast(font_size),
|
||||||
});
|
});
|
||||||
if (!region.found_existing) {
|
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,
|
.font_size_px = run.font_size_px,
|
||||||
});
|
});
|
||||||
if (rendered_glyph.bitmap) |bitmap| {
|
if (rendered_glyph.bitmap) |bitmap| {
|
||||||
var glyph_atlas_region = try pipeline.texture_atlas.reserve(text.state().allocator, rendered_glyph.width, rendered_glyph.height);
|
var glyph_atlas_region = try built.texture_atlas.reserve(allocator, rendered_glyph.width, rendered_glyph.height);
|
||||||
pipeline.texture_atlas.set(glyph_atlas_region, @as([*]const u8, @ptrCast(bitmap.ptr))[0 .. bitmap.len * 4]);
|
built.texture_atlas.set(glyph_atlas_region, @as([*]const u8, @ptrCast(bitmap.ptr))[0 .. bitmap.len * 4]);
|
||||||
texture_update = true;
|
texture_update = true;
|
||||||
|
|
||||||
// Exclude the 1px blank space margin when describing the region of the texture
|
// 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 r = region.value_ptr.*;
|
||||||
const size = vec2(@floatFromInt(r.width), @floatFromInt(r.height));
|
const size = vec2(@floatFromInt(r.width), @floatFromInt(r.height));
|
||||||
try glyphs.append(text.state().allocator, .{
|
try built_text.glyphs.append(allocator, .{
|
||||||
.pos = vec2(
|
.pos = vec2(
|
||||||
origin_x + glyph.offset.x(),
|
origin_x + glyph.offset.x(),
|
||||||
origin_y - (size.y() - glyph.offset.y()),
|
origin_y - (size.y() - glyph.offset.y()),
|
||||||
).divScalar(px_density),
|
).divScalar(px_density),
|
||||||
.size = size.divScalar(px_density),
|
.size = size.divScalar(px_density),
|
||||||
.text_index = 0,
|
.text_index = num_texts,
|
||||||
.uv_pos = vec2(@floatFromInt(r.x), @floatFromInt(r.y)),
|
.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();
|
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?
|
// Every pipeline update, we copy updated glyph and text buffers to the GPU.
|
||||||
if (glyphs.items.len > 0) encoder.writeBuffer(pipeline.glyphs, 0, glyphs.items);
|
try text_pipeline.set(pipeline_id, .num_texts, num_texts);
|
||||||
defer glyphs.deinit(text.state().allocator);
|
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) {
|
if (texture_update) {
|
||||||
// rgba32_pixels
|
// TODO(text): do not assume texture's data_layout and img_size here, instead get it from
|
||||||
// TODO: use proper texture dimensions here
|
// 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 img_size = gpu.Extent3D{ .width = 1024, .height = 1024 };
|
||||||
const data_layout = gpu.Texture.DataLayout{
|
const data_layout = gpu.Texture.DataLayout{
|
||||||
.bytes_per_row = @as(u32, @intCast(img_size.width * 4)),
|
.bytes_per_row = @as(u32, @intCast(img_size.width * 4)),
|
||||||
.rows_per_image = @as(u32, @intCast(img_size.height)),
|
.rows_per_image = @as(u32, @intCast(img_size.height)),
|
||||||
};
|
};
|
||||||
engine.state().queue.writeTexture(
|
core.state().queue.writeTexture(
|
||||||
&.{ .texture = pipeline.texture },
|
&.{ .texture = built.texture },
|
||||||
&data_layout,
|
&data_layout,
|
||||||
&img_size,
|
&img_size,
|
||||||
pipeline.texture_atlas.data,
|
built.texture_atlas.data,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (num_texts > 0 or glyphs.items.len > 0) {
|
||||||
var command = encoder.finish(null);
|
var command = encoder.finish(null);
|
||||||
defer command.release();
|
defer command.release();
|
||||||
|
core.state().queue.submit(&[_]*gpu.CommandBuffer{command});
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
407
src/gfx/TextPipeline.zig
Normal file
407
src/gfx/TextPipeline.zig
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ pub const Atlas = @import("atlas/Atlas.zig");
|
||||||
pub const Sprite = @import("Sprite.zig");
|
pub const Sprite = @import("Sprite.zig");
|
||||||
pub const SpritePipeline = @import("SpritePipeline.zig");
|
pub const SpritePipeline = @import("SpritePipeline.zig");
|
||||||
pub const Text = @import("Text.zig");
|
pub const Text = @import("Text.zig");
|
||||||
|
pub const TextPipeline = @import("TextPipeline.zig");
|
||||||
pub const TextStyle = @import("TextStyle.zig");
|
pub const TextStyle = @import("TextStyle.zig");
|
||||||
|
|
||||||
// Fonts
|
// Fonts
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue