From df8dc33ab65731165fba91afc48367bad6c8ced0 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 23 Jun 2025 13:12:17 -0600 Subject: [PATCH] renderer: unify `image.zig` The code in metal/image.zig and opengl/image.zig was virtually identical save for the texture options, so I've moved that to the GraphicsAPI and unified them in to renderer/image.zig --- pkg/wuffs/src/main.zig | 1 + src/renderer/Metal.zig | 40 ++- src/renderer/OpenGL.zig | 34 ++- src/renderer/generic.zig | 12 +- src/renderer/{metal => }/image.zig | 51 ++-- src/renderer/opengl/image.zig | 423 ----------------------------- 6 files changed, 96 insertions(+), 465 deletions(-) rename src/renderer/{metal => }/image.zig (90%) delete mode 100644 src/renderer/opengl/image.zig diff --git a/pkg/wuffs/src/main.zig b/pkg/wuffs/src/main.zig index f282261c2..be4eb9184 100644 --- a/pkg/wuffs/src/main.zig +++ b/pkg/wuffs/src/main.zig @@ -3,6 +3,7 @@ const std = @import("std"); pub const png = @import("png.zig"); pub const jpeg = @import("jpeg.zig"); pub const swizzle = @import("swizzle.zig"); +pub const Error = @import("error.zig").Error; pub const ImageData = struct { width: u32, diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 0f8d642d0..40ceda5a8 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -29,8 +29,6 @@ pub const Buffer = bufferpkg.Buffer; pub const Texture = @import("metal/Texture.zig"); pub const shaders = @import("metal/shaders.zig"); -pub const imagepkg = @import("metal/image.zig"); - pub const custom_shader_target: shadertoy.Target = .msl; // The fragCoord for Metal shaders is +Y = down. pub const custom_shader_y_is_down = true; @@ -304,6 +302,44 @@ pub inline fn textureOptions(self: Metal) Texture.Options { }; } +/// Pixel format for image texture options. +pub const ImageTextureFormat = enum { + /// 1 byte per pixel grayscale. + gray, + /// 4 bytes per pixel RGBA. + rgba, + /// 4 bytes per pixel BGRA. + bgra, + + fn toPixelFormat( + self: ImageTextureFormat, + srgb: bool, + ) mtl.MTLPixelFormat { + return switch (self) { + .gray => if (srgb) .r8unorm_srgb else .r8unorm, + .rgba => if (srgb) .rgba8unorm_srgb else .rgba8unorm, + .bgra => if (srgb) .bgra8unorm_srgb else .bgra8unorm, + }; + } +}; + +/// Returns the options to use when constructing textures for images. +pub inline fn imageTextureOptions( + self: Metal, + format: ImageTextureFormat, + srgb: bool, +) Texture.Options { + return .{ + .device = self.device, + .pixel_format = format.toPixelFormat(srgb), + .resource_options = .{ + // Indicate that the CPU writes to this resource but never reads it. + .cpu_cache_mode = .write_combined, + .storage_mode = self.default_storage_mode, + }, + }; +} + /// Initializes a Texture suitable for the provided font atlas. pub fn initAtlasTexture( self: *const Metal, diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index cb02a0d75..19cfd0779 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -24,8 +24,6 @@ pub const Buffer = bufferpkg.Buffer; pub const Texture = @import("opengl/Texture.zig"); pub const shaders = @import("opengl/shaders.zig"); -pub const imagepkg = @import("opengl/image.zig"); - pub const custom_shader_target: shadertoy.Target = .glsl; // The fragCoord for OpenGL shaders is +Y = up. pub const custom_shader_y_is_down = false; @@ -401,6 +399,38 @@ pub inline fn textureOptions(self: OpenGL) Texture.Options { }; } +/// Pixel format for image texture options. +pub const ImageTextureFormat = enum { + /// 1 byte per pixel grayscale. + gray, + /// 4 bytes per pixel RGBA. + rgba, + /// 4 bytes per pixel BGRA. + bgra, + + fn toPixelFormat(self: ImageTextureFormat) gl.Texture.Format { + return switch (self) { + .gray => .red, + .rgba => .rgba, + .bgra => .bgra, + }; + } +}; + +/// Returns the options to use when constructing textures for images. +pub inline fn imageTextureOptions( + self: OpenGL, + format: ImageTextureFormat, + srgb: bool, +) Texture.Options { + _ = self; + return .{ + .format = format.toPixelFormat(), + .internal_format = if (srgb) .srgba else .rgba, + .target = .Rectangle, + }; +} + /// Initializes a Texture suitable for the provided font atlas. pub fn initAtlasTexture( self: *const OpenGL, diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index ae31b2ef1..8c42e68f0 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -14,6 +14,10 @@ const link = @import("link.zig"); const cellpkg = @import("cell.zig"); const fgMode = cellpkg.fgMode; const isCovering = cellpkg.isCovering; +const imagepkg = @import("image.zig"); +const Image = imagepkg.Image; +const ImageMap = imagepkg.ImageMap; +const ImagePlacementList = std.ArrayListUnmanaged(imagepkg.Placement); const shadertoy = @import("shadertoy.zig"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; @@ -78,16 +82,10 @@ pub fn Renderer(comptime GraphicsAPI: type) type { const Buffer = GraphicsAPI.Buffer; const Texture = GraphicsAPI.Texture; const RenderPass = GraphicsAPI.RenderPass; + const shaderpkg = GraphicsAPI.shaders; - - const imagepkg = GraphicsAPI.imagepkg; - const Image = imagepkg.Image; - const ImageMap = imagepkg.ImageMap; - const Shaders = shaderpkg.Shaders; - const ImagePlacementList = std.ArrayListUnmanaged(imagepkg.Placement); - /// Allocator that can be used alloc: std.mem.Allocator, diff --git a/src/renderer/metal/image.zig b/src/renderer/image.zig similarity index 90% rename from src/renderer/metal/image.zig rename to src/renderer/image.zig index 1bfa3c621..277ddd8c0 100644 --- a/src/renderer/metal/image.zig +++ b/src/renderer/image.zig @@ -1,16 +1,14 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; -const objc = @import("objc"); const wuffs = @import("wuffs"); -const Metal = @import("../Metal.zig"); -const Texture = Metal.Texture; +const Renderer = @import("../renderer.zig").Renderer; +const GraphicsAPI = Renderer.API; +const Texture = GraphicsAPI.Texture; -const mtl = @import("api.zig"); - -/// Represents a single image placement on the grid. A placement is a -/// request to render an instance of an image. +/// Represents a single image placement on the grid. +/// A placement is a request to render an instance of an image. pub const Placement = struct { /// The image being rendered. This MUST be in the image map. image_id: u32, @@ -174,8 +172,8 @@ pub const Image = union(enum) { // scenarios where there is no existing texture and we can modify // the self pointer directly. const existing: Texture = switch (self.*) { - // For pending, we can free the old data and become pending - // ourselves. + // For pending, we can free the old + // data and become pending ourselves. .pending_gray => |p| { alloc.free(p.dataSlice(1)); self.* = img; @@ -214,8 +212,8 @@ pub const Image = union(enum) { break :existing r[1]; }, - // If we were already pending a replacement, then we free our - // existing pending data and use the same texture. + // If we were already pending a replacement, then we free + // our existing pending data and use the same texture. .replace_gray => |r| existing: { alloc.free(r.pending.dataSlice(1)); break :existing r.texture; @@ -236,9 +234,9 @@ pub const Image = union(enum) { break :existing r.texture; }, - // For both ready and unload_ready, we need to replace the - // texture. We can't do that here, so we just mark ourselves - // for replacement. + // For both ready and unload_ready, we need to replace + // the texture. We can't do that here, so we just mark + // ourselves for replacement. .ready, .unload_ready => |tex| tex, }; @@ -281,6 +279,8 @@ pub const Image = union(enum) { => true, .ready, + .pending_gray, + .pending_gray_alpha, .pending_rgb, .pending_rgba, => false, @@ -290,7 +290,10 @@ pub const Image = union(enum) { /// Converts the image data to a format that can be uploaded to the GPU. /// If the data is already in a format that can be uploaded, this is a /// no-op. - pub fn convert(self: *Image, alloc: Allocator) !void { + pub fn convert(self: *Image, alloc: Allocator) wuffs.Error!void { + // As things stand, we currently convert all images to RGBA before + // uploading to the GPU. This just makes things easier. In the future + // we may want to support other formats. switch (self.*) { .ready, .unload_pending, @@ -302,8 +305,6 @@ pub const Image = union(enum) { .replace_rgba, => {}, // ready - // RGB needs to be converted to RGBA because Metal textures - // don't support RGB. .pending_rgb => |*p| { const data = p.dataSlice(3); const rgba = try wuffs.swizzle.rgbToRgba(alloc, data); @@ -320,7 +321,6 @@ pub const Image = union(enum) { self.* = .{ .replace_rgba = r.* }; }, - // Gray and Gray+Alpha need to be converted to RGBA, too. .pending_gray => |*p| { const data = p.dataSlice(1); const rgba = try wuffs.swizzle.gToRgba(alloc, data); @@ -360,11 +360,8 @@ pub const Image = union(enum) { pub fn upload( self: *Image, alloc: Allocator, - metal: *const Metal, + api: *const GraphicsAPI, ) !void { - const device = metal.device; - const storage_mode = metal.default_storage_mode; - // Convert our data if we have to try self.convert(alloc); @@ -373,15 +370,7 @@ pub const Image = union(enum) { // Create our texture const texture = try Texture.init( - .{ - .device = device, - .pixel_format = .rgba8unorm_srgb, - .resource_options = .{ - // Indicate that the CPU writes to this resource but never reads it. - .cpu_cache_mode = .write_combined, - .storage_mode = storage_mode, - }, - }, + api.imageTextureOptions(.rgba, true), @intCast(p.width), @intCast(p.height), p.data[0 .. p.width * p.height * self.depth()], diff --git a/src/renderer/opengl/image.zig b/src/renderer/opengl/image.zig deleted file mode 100644 index 77779fb8a..000000000 --- a/src/renderer/opengl/image.zig +++ /dev/null @@ -1,423 +0,0 @@ -const std = @import("std"); -const Allocator = std.mem.Allocator; -const assert = std.debug.assert; -const gl = @import("opengl"); -const wuffs = @import("wuffs"); -const OpenGL = @import("../OpenGL.zig"); -const Texture = OpenGL.Texture; - -/// Represents a single image placement on the grid. A placement is a -/// request to render an instance of an image. -pub const Placement = struct { - /// The image being rendered. This MUST be in the image map. - image_id: u32, - - /// The grid x/y where this placement is located. - x: i32, - y: i32, - z: i32, - - /// The width/height of the placed image. - width: u32, - height: u32, - - /// The offset in pixels from the top left of the cell. This is - /// clamped to the size of a cell. - cell_offset_x: u32, - cell_offset_y: u32, - - /// The source rectangle of the placement. - source_x: u32, - source_y: u32, - source_width: u32, - source_height: u32, -}; - -/// The map used for storing images. -pub const ImageMap = std.AutoHashMapUnmanaged(u32, struct { - image: Image, - transmit_time: std.time.Instant, -}); - -/// The state for a single image that is to be rendered. The image can be -/// pending upload or ready to use with a texture. -pub const Image = union(enum) { - /// The image is pending upload to the GPU. The different keys are - /// different formats since some formats aren't accepted by the GPU - /// and require conversion. - /// - /// This data is owned by this union so it must be freed once the - /// image is uploaded. - pending_gray: Pending, - pending_gray_alpha: Pending, - pending_rgb: Pending, - pending_rgba: Pending, - - /// This is the same as the pending states but there is a texture - /// already allocated that we want to replace. - replace_gray: Replace, - replace_gray_alpha: Replace, - replace_rgb: Replace, - replace_rgba: Replace, - - /// The image is uploaded and ready to be used. - ready: Texture, - - /// The image is uploaded but is scheduled to be unloaded. - unload_pending: []u8, - unload_ready: Texture, - unload_replace: struct { []u8, Texture }, - - pub const Replace = struct { - texture: Texture, - pending: Pending, - }; - - /// Pending image data that needs to be uploaded to the GPU. - pub const Pending = struct { - height: u32, - width: u32, - - /// Data is always expected to be (width * height * depth). Depth - /// is based on the union key. - data: [*]u8, - - pub fn dataSlice(self: Pending, d: u32) []u8 { - return self.data[0..self.len(d)]; - } - - pub fn len(self: Pending, d: u32) u32 { - return self.width * self.height * d; - } - }; - - pub fn deinit(self: Image, alloc: Allocator) void { - switch (self) { - .pending_gray => |p| alloc.free(p.dataSlice(1)), - .pending_gray_alpha => |p| alloc.free(p.dataSlice(2)), - .pending_rgb => |p| alloc.free(p.dataSlice(3)), - .pending_rgba => |p| alloc.free(p.dataSlice(4)), - .unload_pending => |data| alloc.free(data), - - .replace_gray => |r| { - alloc.free(r.pending.dataSlice(1)); - r.texture.deinit(); - }, - - .replace_gray_alpha => |r| { - alloc.free(r.pending.dataSlice(2)); - r.texture.deinit(); - }, - - .replace_rgb => |r| { - alloc.free(r.pending.dataSlice(3)); - r.texture.deinit(); - }, - - .replace_rgba => |r| { - alloc.free(r.pending.dataSlice(4)); - r.texture.deinit(); - }, - - .unload_replace => |r| { - alloc.free(r[0]); - r[1].deinit(); - }, - - .ready, - .unload_ready, - => |tex| tex.deinit(), - } - } - - /// Mark this image for unload whatever state it is in. - pub fn markForUnload(self: *Image) void { - self.* = switch (self.*) { - .unload_pending, - .unload_replace, - .unload_ready, - => return, - - .ready => |obj| .{ .unload_ready = obj }, - .pending_gray => |p| .{ .unload_pending = p.dataSlice(1) }, - .pending_gray_alpha => |p| .{ .unload_pending = p.dataSlice(2) }, - .pending_rgb => |p| .{ .unload_pending = p.dataSlice(3) }, - .pending_rgba => |p| .{ .unload_pending = p.dataSlice(4) }, - .replace_gray => |r| .{ .unload_replace = .{ - r.pending.dataSlice(1), r.texture, - } }, - .replace_gray_alpha => |r| .{ .unload_replace = .{ - r.pending.dataSlice(2), r.texture, - } }, - .replace_rgb => |r| .{ .unload_replace = .{ - r.pending.dataSlice(3), r.texture, - } }, - .replace_rgba => |r| .{ .unload_replace = .{ - r.pending.dataSlice(4), r.texture, - } }, - }; - } - - /// Replace the currently pending image with a new one. This will - /// attempt to update the existing texture if it is already allocated. - /// If the texture is not allocated, this will act like a new upload. - /// - /// This function only marks the image for replace. The actual logic - /// to replace is done later. - pub fn markForReplace(self: *Image, alloc: Allocator, img: Image) !void { - assert(img.pending() != null); - - // Get our existing texture. This switch statement will also handle - // scenarios where there is no existing texture and we can modify - // the self pointer directly. - const existing: Texture = switch (self.*) { - // For pending, we can free the old data and become pending ourselves. - .pending_gray => |p| { - alloc.free(p.dataSlice(1)); - self.* = img; - return; - }, - - .pending_gray_alpha => |p| { - alloc.free(p.dataSlice(2)); - self.* = img; - return; - }, - - .pending_rgb => |p| { - alloc.free(p.dataSlice(3)); - self.* = img; - return; - }, - - .pending_rgba => |p| { - alloc.free(p.dataSlice(4)); - self.* = img; - return; - }, - - // If we're marked for unload but we just have pending data, - // this behaves the same as a normal "pending": free the data, - // become new pending. - .unload_pending => |data| { - alloc.free(data); - self.* = img; - return; - }, - - .unload_replace => |r| existing: { - alloc.free(r[0]); - break :existing r[1]; - }, - - // If we were already pending a replacement, then we free our - // existing pending data and use the same texture. - .replace_gray => |r| existing: { - alloc.free(r.pending.dataSlice(1)); - break :existing r.texture; - }, - - .replace_gray_alpha => |r| existing: { - alloc.free(r.pending.dataSlice(2)); - break :existing r.texture; - }, - - .replace_rgb => |r| existing: { - alloc.free(r.pending.dataSlice(3)); - break :existing r.texture; - }, - - .replace_rgba => |r| existing: { - alloc.free(r.pending.dataSlice(4)); - break :existing r.texture; - }, - - // For both ready and unload_ready, we need to replace the - // texture. We can't do that here, so we just mark ourselves - // for replacement. - .ready, .unload_ready => |tex| tex, - }; - - // We now have an existing texture, so set the proper replace key. - self.* = switch (img) { - .pending_gray => |p| .{ .replace_gray = .{ - .texture = existing, - .pending = p, - } }, - - .pending_gray_alpha => |p| .{ .replace_gray_alpha = .{ - .texture = existing, - .pending = p, - } }, - - .pending_rgb => |p| .{ .replace_rgb = .{ - .texture = existing, - .pending = p, - } }, - - .pending_rgba => |p| .{ .replace_rgba = .{ - .texture = existing, - .pending = p, - } }, - - else => unreachable, - }; - } - - /// Returns true if this image is pending upload. - pub fn isPending(self: Image) bool { - return self.pending() != null; - } - - /// Returns true if this image is pending an unload. - pub fn isUnloading(self: Image) bool { - return switch (self) { - .unload_pending, - .unload_ready, - => true, - - .ready, - .pending_gray, - .pending_gray_alpha, - .pending_rgb, - .pending_rgba, - => false, - }; - } - - /// Converts the image data to a format that can be uploaded to the GPU. - /// If the data is already in a format that can be uploaded, this is a - /// no-op. - pub fn convert(self: *Image, alloc: Allocator) !void { - switch (self.*) { - .ready, - .unload_pending, - .unload_replace, - .unload_ready, - => unreachable, // invalid - - .pending_rgba, - .replace_rgba, - => {}, // ready - - // RGB needs to be converted to RGBA because Metal textures - // don't support RGB. - .pending_rgb => |*p| { - const data = p.dataSlice(3); - const rgba = try wuffs.swizzle.rgbToRgba(alloc, data); - alloc.free(data); - p.data = rgba.ptr; - self.* = .{ .pending_rgba = p.* }; - }, - - .replace_rgb => |*r| { - const data = r.pending.dataSlice(3); - const rgba = try wuffs.swizzle.rgbToRgba(alloc, data); - alloc.free(data); - r.pending.data = rgba.ptr; - self.* = .{ .replace_rgba = r.* }; - }, - - // Gray and Gray+Alpha need to be converted to RGBA, too. - .pending_gray => |*p| { - const data = p.dataSlice(1); - const rgba = try wuffs.swizzle.gToRgba(alloc, data); - alloc.free(data); - p.data = rgba.ptr; - self.* = .{ .pending_rgba = p.* }; - }, - - .replace_gray => |*r| { - const data = r.pending.dataSlice(2); - const rgba = try wuffs.swizzle.gToRgba(alloc, data); - alloc.free(data); - r.pending.data = rgba.ptr; - self.* = .{ .replace_rgba = r.* }; - }, - - .pending_gray_alpha => |*p| { - const data = p.dataSlice(2); - const rgba = try wuffs.swizzle.gaToRgba(alloc, data); - alloc.free(data); - p.data = rgba.ptr; - self.* = .{ .pending_rgba = p.* }; - }, - - .replace_gray_alpha => |*r| { - const data = r.pending.dataSlice(2); - const rgba = try wuffs.swizzle.gaToRgba(alloc, data); - alloc.free(data); - r.pending.data = rgba.ptr; - self.* = .{ .replace_rgba = r.* }; - }, - } - } - - /// Upload the pending image to the GPU and change the state of this - /// image to ready. - pub fn upload( - self: *Image, - alloc: Allocator, - opengl: *const OpenGL, - ) !void { - _ = opengl; - - // Convert our data if we have to - try self.convert(alloc); - - // Get our pending info - const p = self.pending().?; - - // Get our format - const formats: struct { - internal: gl.Texture.InternalFormat, - format: gl.Texture.Format, - } = switch (self.*) { - .pending_rgb, .replace_rgb => .{ .internal = .srgb, .format = .rgb }, - .pending_rgba, .replace_rgba => .{ .internal = .srgba, .format = .rgba }, - else => unreachable, - }; - - // Create our texture - const tex = try Texture.init( - .{ - .format = formats.format, - .internal_format = formats.internal, - .target = .Rectangle, - }, - @intCast(p.width), - @intCast(p.height), - p.data[0 .. p.width * p.height * self.depth()], - ); - - // Uploaded. We can now clear our data and change our state. - self.deinit(alloc); - self.* = .{ .ready = tex }; - } - - /// Our pixel depth - fn depth(self: Image) u32 { - return switch (self) { - .pending_rgb => 3, - .pending_rgba => 4, - .replace_rgb => 3, - .replace_rgba => 4, - else => unreachable, - }; - } - - /// Returns true if this image is in a pending state and requires upload. - fn pending(self: Image) ?Pending { - return switch (self) { - .pending_rgb, - .pending_rgba, - => |p| p, - - .replace_rgb, - .replace_rgba, - => |r| r.pending, - - else => null, - }; - } -};