481 lines
13 KiB
Zig
481 lines
13 KiB
Zig
const sokol = @import("sokol");
|
|
const sg = sokol.gfx;
|
|
const sglue = sokol.glue;
|
|
const slog = sokol.log;
|
|
const sapp = sokol.app;
|
|
const simgui = sokol.imgui;
|
|
const sgl = sokol.gl;
|
|
|
|
const Math = @import("./math.zig");
|
|
const Vec2 = Math.Vec2;
|
|
const Vec4 = Math.Vec4;
|
|
const rgb = Math.rgb;
|
|
const Rect = Math.Rect;
|
|
|
|
const std = @import("std");
|
|
const log = std.log.scoped(.graphics);
|
|
const assert = std.debug.assert;
|
|
|
|
const imgui = @import("imgui.zig");
|
|
const tracy = @import("tracy");
|
|
const fontstash = @import("./fontstash/root.zig");
|
|
pub const Font = fontstash.Font;
|
|
|
|
const GraphicsFrame = @import("./frame.zig").Graphics;
|
|
|
|
// TODO: Seems that there is a vertical jitter bug when resizing a window in OpenGL. Seems like a driver bug.
|
|
// From other peoples research it seems that disabling vsync when a resize event occurs fixes it.
|
|
// Maybe a patch for sokol could be made?
|
|
// More info:
|
|
// * https://github.com/libsdl-org/SDL/issues/11618
|
|
// * https://github.com/nimgl/nimgl/issues/59
|
|
|
|
const Options = struct {
|
|
const ImguiFont = struct {
|
|
ttf_data: []const u8,
|
|
size: f32 = 16
|
|
};
|
|
|
|
allocator: std.mem.Allocator,
|
|
logger: sg.Logger = .{},
|
|
imgui_font: ?ImguiFont = null
|
|
};
|
|
|
|
pub const Command = union(enum) {
|
|
pub const DrawRectangle = struct {
|
|
rect: Rect,
|
|
color: Vec4,
|
|
sprite: ?Sprite = null,
|
|
rotation: f32 = 0,
|
|
origin: Vec2 = .init(0, 0),
|
|
};
|
|
|
|
set_scissor: Rect,
|
|
draw_rectangle: DrawRectangle,
|
|
draw_line: struct {
|
|
pos1: Vec2,
|
|
pos2: Vec2,
|
|
color: Vec4,
|
|
width: f32
|
|
},
|
|
draw_text: struct {
|
|
pos: Vec2,
|
|
text: []const u8,
|
|
font: Font.Id,
|
|
size: f32,
|
|
color: Vec4,
|
|
},
|
|
push_transformation: struct {
|
|
translation: Vec2,
|
|
scale: Vec2
|
|
},
|
|
pop_transformation: void
|
|
};
|
|
|
|
const Texture = struct {
|
|
image: sg.Image,
|
|
view: sg.View,
|
|
info: Info,
|
|
|
|
const Info = struct {
|
|
width: u32,
|
|
height: u32,
|
|
};
|
|
|
|
const Id = enum(u32) { _ };
|
|
const Data = struct {
|
|
width: u32,
|
|
height: u32,
|
|
rgba: [*]u8
|
|
};
|
|
};
|
|
pub const TextureId = Texture.Id;
|
|
pub const TextureInfo = Texture.Info;
|
|
|
|
pub const Sprite = struct {
|
|
texture: TextureId,
|
|
uv: Rect,
|
|
};
|
|
|
|
var gpa: std.mem.Allocator = undefined;
|
|
|
|
var main_pipeline: sgl.Pipeline = .{};
|
|
var linear_sampler: sg.Sampler = .{};
|
|
var nearest_sampler: sg.Sampler = .{};
|
|
var font_context: fontstash.Context = undefined;
|
|
var textures: std.ArrayList(Texture) = .empty;
|
|
|
|
var scale_stack_buffer: [32]Vec2 = undefined;
|
|
var scale_stack: std.ArrayList(Vec2) = .empty;
|
|
|
|
pub fn init(options: Options) !void {
|
|
gpa = options.allocator;
|
|
|
|
sg.setup(.{
|
|
.logger = options.logger,
|
|
.environment = sglue.environment(),
|
|
});
|
|
|
|
sgl.setup(.{
|
|
.logger = .{
|
|
.func = options.logger.func,
|
|
.user_data = options.logger.user_data
|
|
}
|
|
});
|
|
|
|
main_pipeline = sgl.makePipeline(.{
|
|
.colors = init: {
|
|
var colors: [8]sg.ColorTargetState = @splat(.{});
|
|
colors[0] = .{
|
|
.blend = .{
|
|
.enabled = true,
|
|
.src_factor_rgb = .SRC_ALPHA,
|
|
.dst_factor_rgb = .ONE_MINUS_SRC_ALPHA,
|
|
.op_rgb = .ADD,
|
|
.src_factor_alpha = .ONE,
|
|
.dst_factor_alpha = .ONE_MINUS_SRC_ALPHA,
|
|
.op_alpha = .ADD,
|
|
},
|
|
};
|
|
break :init colors;
|
|
},
|
|
});
|
|
|
|
imgui.setup(options.allocator, .{
|
|
.logger = .{
|
|
.func = options.logger.func,
|
|
.user_data = options.logger.user_data
|
|
},
|
|
.no_default_font = options.imgui_font != null,
|
|
|
|
// TODO: Figure out a way to make imgui play nicely with UI
|
|
// Ideally when mouse is inside a Imgui window, then the imgui cursor should be used.
|
|
// Otherwise our own cursor should be used.
|
|
.disable_set_mouse_cursor = true
|
|
});
|
|
|
|
if (options.imgui_font) |imgui_font| {
|
|
imgui.addFont(imgui_font.ttf_data, imgui_font.size);
|
|
}
|
|
|
|
linear_sampler = sg.makeSampler(.{
|
|
.min_filter = .LINEAR,
|
|
.mag_filter = .LINEAR,
|
|
.mipmap_filter = .LINEAR,
|
|
.label = "linear-sampler",
|
|
});
|
|
|
|
nearest_sampler = sg.makeSampler(.{
|
|
.min_filter = .NEAREST,
|
|
.mag_filter = .NEAREST,
|
|
.mipmap_filter = .NEAREST,
|
|
.label = "nearest-sampler",
|
|
});
|
|
|
|
const dpi_scale = sapp.dpiScale();
|
|
const atlas_size = 512;
|
|
const atlas_dim = std.math.ceilPowerOfTwoAssert(u32, @intFromFloat(atlas_size * dpi_scale));
|
|
font_context = try fontstash.Context.init(.{
|
|
.width = @intCast(atlas_dim),
|
|
.height = @intCast(atlas_dim),
|
|
});
|
|
}
|
|
|
|
pub fn deinit() void {
|
|
textures.deinit(gpa);
|
|
imgui.shutdown();
|
|
font_context.deinit();
|
|
sgl.shutdown();
|
|
sg.shutdown();
|
|
}
|
|
|
|
pub fn drawCommand(command: Command) void {
|
|
switch(command) {
|
|
.push_transformation => |opts| {
|
|
pushTransform(opts.translation, opts.scale);
|
|
// font_resolution_scale = font_resolution_scale.multiply(opts.scale);
|
|
},
|
|
.pop_transformation => {
|
|
popTransform();
|
|
},
|
|
.draw_rectangle => |opts| {
|
|
drawRectangle(opts);
|
|
},
|
|
.set_scissor => |opts| {
|
|
sgl.scissorRectf(
|
|
opts.pos.x,
|
|
opts.pos.y,
|
|
opts.size.x,
|
|
opts.size.y,
|
|
true
|
|
);
|
|
},
|
|
.draw_line => |opts| {
|
|
drawLine(
|
|
opts.pos1,
|
|
opts.pos2,
|
|
opts.color,
|
|
opts.width
|
|
);
|
|
},
|
|
.draw_text => |opts| {
|
|
const font_resolution_scale = scale_stack.getLast();
|
|
|
|
sgl.pushMatrix();
|
|
defer sgl.popMatrix();
|
|
|
|
sgl.scale(1/font_resolution_scale.x, 1/font_resolution_scale.y, 1);
|
|
|
|
font_context.setFont(opts.font);
|
|
font_context.setSize(opts.size * font_resolution_scale.y);
|
|
font_context.setAlign(.{ .x = .left, .y = .top });
|
|
font_context.setSpacing(0);
|
|
|
|
const r: u8 = @intFromFloat(opts.color.x * 255);
|
|
const g: u8 = @intFromFloat(opts.color.y * 255);
|
|
const b: u8 = @intFromFloat(opts.color.z * 255);
|
|
const a: u8 = @intFromFloat(opts.color.w * 255);
|
|
const color: u32 = r | (@as(u32, g) << 8) | (@as(u32, b) << 16) | (@as(u32, a) << 24);
|
|
font_context.setColor(color);
|
|
font_context.drawText(
|
|
opts.pos.x * font_resolution_scale.x,
|
|
opts.pos.y * font_resolution_scale.y,
|
|
opts.text
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn drawCommands(commands: []const Command) void {
|
|
for (commands) |command| {
|
|
drawCommand(command);
|
|
}
|
|
}
|
|
|
|
pub fn beginFrame() void {
|
|
const zone = tracy.initZone(@src(), .{ });
|
|
defer zone.deinit();
|
|
|
|
imgui.newFrame(.{
|
|
.width = sapp.width(),
|
|
.height = sapp.height(),
|
|
.delta_time = sapp.frameDuration(),
|
|
.dpi_scale = sapp.dpiScale()
|
|
});
|
|
|
|
scale_stack = .initBuffer(&scale_stack_buffer);
|
|
scale_stack.appendAssumeCapacity(.init(1, 1));
|
|
|
|
font_context.clearState();
|
|
sgl.defaults();
|
|
sgl.matrixModeProjection();
|
|
sgl.ortho(0, sapp.widthf(), sapp.heightf(), 0, -1, 1);
|
|
sgl.loadPipeline(main_pipeline);
|
|
}
|
|
|
|
pub fn endFrame(clear_color: Vec4) void {
|
|
const zone = tracy.initZone(@src(), .{ });
|
|
defer zone.deinit();
|
|
|
|
var pass_action: sg.PassAction = .{};
|
|
|
|
pass_action.colors[0] = sg.ColorAttachmentAction{
|
|
.load_action = .CLEAR,
|
|
.clear_value = .{
|
|
.r = clear_color.x,
|
|
.g = clear_color.y,
|
|
.b = clear_color.z,
|
|
.a = clear_color.w
|
|
}
|
|
};
|
|
|
|
font_context.flush();
|
|
|
|
{
|
|
sg.beginPass(.{
|
|
.action = pass_action,
|
|
.swapchain = sglue.swapchain()
|
|
});
|
|
defer sg.endPass();
|
|
|
|
sgl.draw();
|
|
|
|
imgui.render();
|
|
}
|
|
sg.commit();
|
|
}
|
|
|
|
fn pushTransform(translation: Vec2, scale: Vec2) void {
|
|
sgl.pushMatrix();
|
|
sgl.translate(translation.x, translation.y, 0);
|
|
sgl.scale(scale.x, scale.y, 1);
|
|
|
|
scale_stack.appendAssumeCapacity(scale_stack.getLast().multiply(scale));
|
|
}
|
|
|
|
fn popTransform() void {
|
|
sgl.popMatrix();
|
|
_ = scale_stack.pop().?;
|
|
}
|
|
|
|
const Vertex = struct {
|
|
pos: Vec2,
|
|
uv: Vec2
|
|
};
|
|
|
|
fn drawQuad(quad: [4]Vertex, color: Vec4, texture_id: TextureId) void {
|
|
sgl.enableTexture();
|
|
defer sgl.disableTexture();
|
|
|
|
const view = textures.items[@intFromEnum(texture_id)].view;
|
|
// TODO: Make sampler configurable
|
|
sgl.texture(view, nearest_sampler);
|
|
|
|
sgl.beginQuads();
|
|
defer sgl.end();
|
|
|
|
sgl.c4f(color.x, color.y, color.z, color.w);
|
|
for (quad) |vertex| {
|
|
const pos = vertex.pos;
|
|
const uv = vertex.uv;
|
|
sgl.v2fT2f(pos.x, pos.y, uv.x, uv.y);
|
|
}
|
|
}
|
|
|
|
fn drawQuadNoUVs(quad: [4]Vec2, color: Vec4) void {
|
|
sgl.beginQuads();
|
|
defer sgl.end();
|
|
|
|
sgl.c4f(color.x, color.y, color.z, color.w);
|
|
for (quad) |pos| {
|
|
sgl.v2f(pos.x, pos.y);
|
|
}
|
|
}
|
|
|
|
fn drawRectangle(opts: Command.DrawRectangle) void {
|
|
const pos = opts.rect.pos;
|
|
const size = opts.rect.size;
|
|
|
|
const top_left = Vec2.init(0, 0).rotateAround(opts.rotation, opts.origin);
|
|
const top_right = Vec2.init(size.x, 0).rotateAround(opts.rotation, opts.origin);
|
|
const bottom_right = size.rotateAround(opts.rotation, opts.origin);
|
|
const bottom_left = Vec2.init(0, size.y).rotateAround(opts.rotation, opts.origin);
|
|
|
|
if (opts.sprite) |sprite| {
|
|
const uv = sprite.uv;
|
|
const quad = [4]Vertex{
|
|
.{
|
|
.pos = pos.add(top_left),
|
|
.uv = .init(uv.left(), uv.top())
|
|
},
|
|
.{
|
|
.pos = pos.add(top_right),
|
|
.uv = .init(uv.right(), uv.top())
|
|
},
|
|
.{
|
|
.pos = pos.add(bottom_right),
|
|
.uv = .init(uv.right(), uv.bottom())
|
|
},
|
|
.{
|
|
.pos = pos.add(bottom_left),
|
|
.uv = .init(uv.left(), uv.bottom())
|
|
}
|
|
};
|
|
drawQuad(quad, opts.color, sprite.texture);
|
|
} else {
|
|
const quad = .{
|
|
pos.add(top_left),
|
|
pos.add(top_right),
|
|
pos.add(bottom_right),
|
|
pos.add(bottom_left)
|
|
};
|
|
drawQuadNoUVs(quad, opts.color);
|
|
}
|
|
}
|
|
|
|
fn drawLine(from: Vec2, to: Vec2, color: Vec4, width: f32) void {
|
|
const step = to.sub(from).normalized().multiplyScalar(width/2);
|
|
|
|
const top_left = from.add(step.rotateLeft90());
|
|
const bottom_left = from.add(step.rotateRight90());
|
|
const top_right = to.add(step.rotateLeft90());
|
|
const bottom_right = to.add(step.rotateRight90());
|
|
|
|
drawQuadNoUVs(
|
|
.{ top_right, top_left, bottom_left, bottom_right },
|
|
color
|
|
);
|
|
}
|
|
|
|
pub fn addFont(name: [*c]const u8, data: []const u8) !Font.Id {
|
|
return try font_context.addFont(name, data);
|
|
}
|
|
|
|
fn makeView(image: sg.Image) !sg.View {
|
|
const image_view = sg.makeView(.{
|
|
.texture = .{ .image = image }
|
|
});
|
|
if (image_view.id == sg.invalid_id) {
|
|
return error.InvalidView;
|
|
}
|
|
return image_view;
|
|
}
|
|
|
|
fn makeImageWithMipMaps(mipmaps: []const Texture.Data) !sg.Image {
|
|
if (mipmaps.len == 0) {
|
|
return error.NoMipMaps;
|
|
}
|
|
|
|
var data: sg.ImageData = .{};
|
|
var mip_levels: std.ArrayListUnmanaged(sg.Range) = .initBuffer(&data.mip_levels);
|
|
|
|
for (mipmaps) |mipmap| {
|
|
try mip_levels.appendBounded(.{
|
|
.ptr = mipmap.rgba,
|
|
.size = mipmap.width * mipmap.height * 4
|
|
});
|
|
}
|
|
|
|
const image = sg.makeImage(.{
|
|
.width = @intCast(mipmaps[0].width),
|
|
.height = @intCast(mipmaps[0].height),
|
|
.pixel_format = .RGBA8,
|
|
.usage = .{
|
|
.immutable = true
|
|
},
|
|
.num_mipmaps = @intCast(mip_levels.items.len),
|
|
.data = data
|
|
});
|
|
if (image.id == sg.invalid_id) {
|
|
return error.InvalidImage;
|
|
}
|
|
|
|
return image;
|
|
}
|
|
|
|
pub fn addTexture(mipmaps: []const Texture.Data) !TextureId {
|
|
const image = try makeImageWithMipMaps(mipmaps);
|
|
errdefer sg.deallocImage(image);
|
|
|
|
const view = try makeView(image);
|
|
errdefer sg.deallocView(view);
|
|
|
|
assert(mipmaps.len > 0);
|
|
const index = textures.items.len;
|
|
try textures.append(gpa, .{
|
|
.image = image,
|
|
.view = view,
|
|
.info = .{
|
|
.width = mipmaps[0].width,
|
|
.height = mipmaps[0].height,
|
|
}
|
|
});
|
|
|
|
return @enumFromInt(index);
|
|
}
|
|
|
|
pub fn getTextureInfo(id: TextureId) TextureInfo {
|
|
const texture = textures.items[@intFromEnum(id)];
|
|
return texture.info;
|
|
}
|