sokoban-v1/src/engine/graphics.zig
2026-02-07 14:57:47 +02:00

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;
}