renderer-mvp/asset-pipeline #53

Merged
kayomn merged 6 commits from renderer-mvp/asset-pipeline into main 2024-06-23 04:37:40 +02:00
7 changed files with 317 additions and 163 deletions
Showing only changes of commit ee88f58e53 - Show all commits

View File

@ -2,6 +2,8 @@ pub const ascii = @import("./ascii.zig");
pub const dag = @import("./dag.zig"); pub const dag = @import("./dag.zig");
pub const files = @import("./files.zig");
pub const hashes = @import("./hashes.zig"); pub const hashes = @import("./hashes.zig");
pub const heap = @import("./heap.zig"); pub const heap = @import("./heap.zig");

124
src/coral/files.zig Normal file
View File

@ -0,0 +1,124 @@
const builtin = @import("builtin");
const io = @import("./io.zig");
const std = @import("std");
pub const Error = error {
FileNotFound,
FileInaccessible,
};
pub const Stat = struct {
size: u64,
};
pub const Storage = struct {
userdata: *anyopaque,
vtable: *const VTable,
pub const VTable = struct {
stat: *const fn (*anyopaque, []const u8) Error!Stat,
read: *const fn (*anyopaque, []const u8, usize, []io.Byte) Error!usize,
};
pub fn read_bytes(self: Storage, path: []const u8, offset: usize, output: []io.Byte) Error!usize {
return self.vtable.read(self.userdata, path, offset, output);
}
pub fn read_foreign(self: Storage, path: []const u8, offset: usize, comptime Type: type) Error!?Type {
const decoded = (try self.read_native(path, offset, Type)) orelse {
return null;
};
return switch (@typeInfo(Type)) {
.Struct => std.mem.byteSwapAllFields(Type, &decoded),
else => @byteSwap(decoded),
};
}
pub fn read_native(self: Storage, path: []const u8, offset: usize, comptime Type: type) Error!?Type {
var buffer = @as([@sizeOf(Type)]io.Byte, undefined);
if (try self.vtable.read(self.userdata, path, offset, &buffer) != buffer.len) {
return null;
}
return @as(*align(1) const Type, @ptrCast(&buffer)).*;
}
pub const read_little = switch (native_endian) {
.little => read_native,
.big => read_foreign,
};
pub const read_big = switch (native_endian) {
.little => read_foreign,
.big => read_native,
};
};
pub const bundle = init: {
const Bundle = struct {
fn full_path(path: []const u8) Error![4095:0]u8 {
var buffer = [_:0]u8{0} ** 4095;
_ = std.fs.cwd().realpath(path, &buffer) catch {
return error.FileInaccessible;
};
return buffer;
}
fn read(_: *anyopaque, path: []const u8, offset: usize, output: []io.Byte) Error!usize {
var file = std.fs.openFileAbsoluteZ(&(try full_path(path)), .{.mode = .read_only}) catch |open_error| {
return switch (open_error) {
error.FileNotFound => error.FileNotFound,
else => error.FileInaccessible,
};
};
defer file.close();
if (offset != 0) {
file.seekTo(offset) catch {
return error.FileInaccessible;
};
}
return file.read(output) catch error.FileInaccessible;
}
fn stat(_: *anyopaque, path: []const u8) Error!Stat {
const file_stat = get: {
var file = std.fs.openFileAbsoluteZ(&(try full_path(path)), .{.mode = .read_only}) catch |open_error| {
return switch (open_error) {
error.FileNotFound => error.FileNotFound,
else => error.FileInaccessible,
};
};
defer file.close();
break: get file.stat() catch {
return error.FileInaccessible;
};
};
return .{
.size = file_stat.size,
};
}
};
break: init Storage{
.userdata = undefined,
.vtable = &.{
.stat = Bundle.stat,
.read = Bundle.read,
},
};
};
const native_endian = builtin.cpu.arch.endian();

View File

@ -6,23 +6,30 @@ const slices = @import("./slices.zig");
const std = @import("std"); const std = @import("std");
pub const Byte = u8; pub const Writable = struct {
kayomn marked this conversation as resolved
Review

Unused addition.

Unused addition.
data: []Byte,
pub const Decoder = coral.io.Functor(coral.io.Error!void, &.{[]coral.Byte}); pub fn writer(self: *Writable) Writer {
return Writer.bind(Writable, self, write);
}
fn write(self: *Writable, buffer: []const u8) !usize {
const range = @min(buffer.len, self.data.len);
@memcpy(self.data[0 .. range], buffer[0 .. range]);
self.data = self.data[range ..];
return buffer.len;
}
};
pub const Byte = u8;
pub const Error = error { pub const Error = error {
UnavailableResource, UnavailableResource,
}; };
pub fn FixedBuffer(comptime len: usize, comptime default_value: anytype) type {
const Value = @TypeOf(default_value);
return struct {
filled: usize = 0,
values: [len]Value = [_]Value{default_value} ** len,
};
}
pub fn Functor(comptime Output: type, comptime input_types: []const type) type { pub fn Functor(comptime Output: type, comptime input_types: []const type) type {
const InputTuple = std.meta.Tuple(input_types); const InputTuple = std.meta.Tuple(input_types);
@ -119,20 +126,6 @@ pub fn Generator(comptime Output: type, comptime input_types: []const type) type
}; };
} }
pub const NullWritable = struct {
written: usize = 0,
pub fn writer(self: *NullWritable) Writer {
return Writer.bind(NullWritable, self, write);
}
pub fn write(self: *NullWritable, buffer: []const u8) !usize {
self.written += buffer.len;
return buffer.len;
}
};
pub const PrintError = Error || error { pub const PrintError = Error || error {
IncompleteWrite, IncompleteWrite,
}; };
@ -141,8 +134,6 @@ pub const Reader = Generator(Error!usize, &.{[]coral.Byte});
pub const Writer = Generator(Error!usize, &.{[]const coral.Byte}); pub const Writer = Generator(Error!usize, &.{[]const coral.Byte});
const native_endian = builtin.cpu.arch.endian();
pub fn alloc_read(input: coral.io.Reader, allocator: std.mem.Allocator) []coral.Byte { pub fn alloc_read(input: coral.io.Reader, allocator: std.mem.Allocator) []coral.Byte {
const buffer = coral.Stack(coral.Byte){.allocator = allocator}; const buffer = coral.Stack(coral.Byte){.allocator = allocator};
@ -153,20 +144,6 @@ pub fn alloc_read(input: coral.io.Reader, allocator: std.mem.Allocator) []coral.
return buffer.to_allocation(streamed); return buffer.to_allocation(streamed);
} }
pub fn are_equal(a: []const Byte, b: []const Byte) bool {
if (a.len != b.len) {
return false;
}
for (0 .. a.len) |i| {
if (a[i] != b[i]) {
return false;
}
}
return true;
}
pub const bits_per_byte = 8; pub const bits_per_byte = 8;
pub fn bytes_of(value: anytype) []const Byte { pub fn bytes_of(value: anytype) []const Byte {
@ -179,14 +156,6 @@ pub fn bytes_of(value: anytype) []const Byte {
}; };
} }
pub fn ends_with(haystack: []const Byte, needle: []const Byte) bool {
if (needle.len > haystack.len) {
return false;
}
return are_equal(haystack[haystack.len - needle.len ..], needle);
}
pub fn print(writer: Writer, utf8: []const u8) PrintError!void { pub fn print(writer: Writer, utf8: []const u8) PrintError!void {
if (try writer.yield(.{utf8}) != utf8.len) { if (try writer.yield(.{utf8}) != utf8.len) {
return error.IncompleteWrite; return error.IncompleteWrite;
@ -208,35 +177,6 @@ pub fn skip_n(input: Reader, distance: u64) Error!void {
} }
} }
pub fn read_foreign(input: Reader, comptime Type: type) Error!Type {
const decoded = try read_native(input, Type);
return switch (@typeInfo(input)) {
.Struct => std.mem.byteSwapAllFields(Type, &decoded),
else => @byteSwap(decoded),
};
}
pub fn read_native(input: Reader, comptime Type: type) Error!Type {
var buffer = @as([@sizeOf(Type)]coral.Byte, undefined);
if (try input.yield(.{&buffer}) != buffer.len) {
return error.UnavailableResource;
}
return @as(*align(1) const Type, @ptrCast(&buffer)).*;
}
pub const read_little = switch (native_endian) {
.little => read_native,
.big => read_foreign,
};
pub const read_big = switch (native_endian) {
.little => read_foreign,
.big => read_native,
};
pub fn slice_sentineled(comptime sen: anytype, ptr: [*:sen]const @TypeOf(sen)) [:sen]const @TypeOf(sen) { pub fn slice_sentineled(comptime sen: anytype, ptr: [*:sen]const @TypeOf(sen)) [:sen]const @TypeOf(sen) {
var len = @as(usize, 0); var len = @as(usize, 0);

View File

@ -6,6 +6,7 @@ const ona = @import("ona");
const Actors = struct { const Actors = struct {
instances: coral.stack.Sequential(ona.gfx.Point2D) = .{.allocator = coral.heap.allocator}, instances: coral.stack.Sequential(ona.gfx.Point2D) = .{.allocator = coral.heap.allocator},
quad_mesh_2d: ona.gfx.Handle = .none,
body_texture: ona.gfx.Handle = .none, body_texture: ona.gfx.Handle = .none,
}; };
@ -24,6 +25,7 @@ pub fn main() !void {
fn load(display: coral.Write(ona.gfx.Display), actors: coral.Write(Actors), assets: coral.Write(ona.gfx.Assets)) !void { fn load(display: coral.Write(ona.gfx.Display), actors: coral.Write(Actors), assets: coral.Write(ona.gfx.Assets)) !void {
display.res.width, display.res.height = .{1280, 720}; display.res.width, display.res.height = .{1280, 720};
actors.res.body_texture = try assets.res.open_file("actor.bmp"); actors.res.body_texture = try assets.res.open_file("actor.bmp");
actors.res.quad_mesh_2d = try assets.res.open_quad_mesh_2d(@splat(1));
try actors.res.instances.push_grow(.{0, 0}); try actors.res.instances.push_grow(.{0, 0});
} }
@ -32,11 +34,11 @@ fn exit(actors: coral.Write(Actors)) void {
actors.res.instances.deinit(); actors.res.instances.deinit();
} }
fn render(queue: ona.gfx.Queue, actors: coral.Write(Actors), assets: coral.Read(ona.gfx.Assets)) !void { fn render(queue: ona.gfx.Queue, actors: coral.Write(Actors)) !void {
for (actors.res.instances.values) |instance| { for (actors.res.instances.values) |instance| {
try queue.commands.append(.{ try queue.commands.append(.{
.instance_2d = .{ .instance_2d = .{
.mesh_2d = assets.res.primitives.quad_mesh, .mesh_2d = actors.res.quad_mesh_2d,
.texture = actors.res.body_texture, .texture = actors.res.body_texture,
.transform = .{ .transform = .{

View File

@ -14,41 +14,99 @@ const std = @import("std");
pub const Assets = struct { pub const Assets = struct {
context: device.Context, context: device.Context,
primitives: Primitives,
formats: coral.stack.Sequential(Format), formats: coral.stack.Sequential(Format),
staging_arena: std.heap.ArenaAllocator,
pub const Format = struct { pub const Format = struct {
extension: []const u8, extension: []const u8,
open: *const fn ([]const u8) std.mem.Allocator.Error!Handle, open_file: *const fn (*std.heap.ArenaAllocator, []const u8) Error!Desc,
pub const Error = std.mem.Allocator.Error || coral.files.Error || error {
Unsupported,
};
}; };
const Primitives = struct { pub fn open_file(self: *Assets, path: []const u8) (std.mem.Allocator.Error || Format.Error)!Handle {
quad_mesh: Handle, defer {
}; const max_cache_size = 536870912;
pub fn close(self: *Assets, handle: Handle) void { if (!self.staging_arena.reset(.{.retain_with_limit = max_cache_size})) {
return self.context.close(handle); std.log.warn("failed to retain staging arena size of {} bytes", .{max_cache_size});
} }
}
pub fn open_file(self: *Assets, path: []const u8) std.mem.Allocator.Error!Handle {
for (self.formats.values) |format| { for (self.formats.values) |format| {
if (!std.mem.endsWith(u8, path, format.extension)) { if (!std.mem.endsWith(u8, path, format.extension)) {
continue; continue;
} }
return format.open(path); return self.context.open(try format.open_file(&self.staging_arena, path));
} }
return .none; return .none;
} }
pub fn open_mesh_2d(self: *Assets, mesh_2d: Mesh2D) std.mem.Allocator.Error!Handle { pub fn open_quad_mesh_2d(self: *Assets, extents: Point2D) std.mem.Allocator.Error!Handle {
return self.context.open(.{.mesh_2d = mesh_2d}); const width, const height = extents / @as(Point2D, @splat(2));
return self.context.open(.{
.mesh_2d = .{
.indices = &.{0, 1, 2, 0, 2, 3},
.vertices = &.{
.{.xy = .{-width, height}, .uv = .{0, 1}},
.{.xy = .{width, height}, .uv = .{1, 1}},
.{.xy = .{width, -height}, .uv = .{1, 0}},
.{.xy = .{-width, -height}, .uv = .{0, 0}},
},
},
});
} }
}; };
pub const Color = @Vector(4, f32); pub const Color = @Vector(4, f32);
pub const Desc = union (enum) {
texture: Texture,
mesh_2d: Mesh2D,
pub const Mesh2D = struct {
vertices: []const Vertex,
indices: []const u16,
pub const Vertex = struct {
xy: Point2D,
uv: Point2D,
};
};
pub const Texture = struct {
data: []const coral.io.Byte,
width: u16,
format: Format,
access: Access,
pub const Access = enum {
static,
};
pub const Format = enum {
rgba8888,
bgra8888,
argb8888,
rgb888,
bgr888,
pub fn byte_size(self: Format) usize {
return switch (self) {
.rgba8888, .bgra8888, .argb8888 => 4,
.rgb888, .bgr888 => 3,
};
}
};
};
};
pub const Display = struct { pub const Display = struct {
width: u16 = 1280, width: u16 = 1280,
height: u16 = 720, height: u16 = 720,
@ -85,16 +143,6 @@ pub const Input = union (enum) {
pub const Point2D = @Vector(2, f32); pub const Point2D = @Vector(2, f32);
pub const Mesh2D = struct {
vertices: []const Vertex,
indices: []const u16,
pub const Vertex = struct {
xy: Point2D,
uv: Point2D,
};
};
pub const Queue = struct { pub const Queue = struct {
commands: *device.RenderList, commands: *device.RenderList,
@ -104,6 +152,10 @@ pub const Queue = struct {
pub fn bind(_: coral.system.BindContext) std.mem.Allocator.Error!State { pub fn bind(_: coral.system.BindContext) std.mem.Allocator.Error!State {
// TODO: Review how good of an idea this global state is, even if bind is guaranteed to always be ran on main. // TODO: Review how good of an idea this global state is, even if bind is guaranteed to always be ran on main.
if (renders.is_empty()) {
renders = .{.allocator = coral.heap.allocator};
}
const command_index = renders.len(); const command_index = renders.len();
try renders.push_grow(device.RenderChain.init(coral.heap.allocator)); try renders.push_grow(device.RenderChain.init(coral.heap.allocator));
@ -121,39 +173,21 @@ pub const Queue = struct {
pub fn unbind(state: *State) void { pub fn unbind(state: *State) void {
std.debug.assert(!renders.is_empty()); std.debug.assert(!renders.is_empty());
std.mem.swap(device.RenderChain, &renders.values[state.command_index], renders.get_ptr().?);
const render = &renders.values[state.command_index];
render.deinit();
std.mem.swap(device.RenderChain, render, renders.get_ptr().?);
std.debug.assert(renders.pop()); std.debug.assert(renders.pop());
if (renders.is_empty()) {
Queue.renders.deinit();
}
} }
var renders = coral.stack.Sequential(device.RenderChain){.allocator = coral.heap.allocator}; var renders = coral.stack.Sequential(device.RenderChain){.allocator = coral.heap.allocator};
}; };
pub const Texture = struct {
data: []const coral.io.Byte,
width: u16,
format: Format,
access: Access,
pub const Access = enum {
static,
};
pub const Format = enum {
rgba8888,
bgra8888,
argb8888,
rgb888,
bgr888,
pub fn byte_size(self: Format) usize {
return switch (self) {
.rgba8888, .bgra8888, .argb8888 => 4,
.rgb888, .bgr888 => 3,
};
}
};
};
pub const Transform2D = extern struct { pub const Transform2D = extern struct {
xbasis: Point2D = .{1, 0}, xbasis: Point2D = .{1, 0},
ybasis: Point2D = .{0, 1}, ybasis: Point2D = .{0, 1},
@ -163,7 +197,7 @@ pub const Transform2D = extern struct {
const builtin_formats = [_]Assets.Format{ const builtin_formats = [_]Assets.Format{
.{ .{
.extension = "bmp", .extension = "bmp",
.open = formats.open_bmp, .open_file = formats.load_bmp,
}, },
}; };
@ -210,24 +244,8 @@ pub fn setup(world: *coral.World, events: App.Events) (error {Unsupported} || st
try registered_formats.grow(builtin_formats.len); try registered_formats.grow(builtin_formats.len);
std.debug.assert(registered_formats.push_all(&builtin_formats)); std.debug.assert(registered_formats.push_all(&builtin_formats));
const half_extent = 0.5;
try world.set_resource(.none, Assets{ try world.set_resource(.none, Assets{
.primitives = .{ .staging_arena = std.heap.ArenaAllocator.init(coral.heap.allocator),
.quad_mesh = try context.open(.{
.mesh_2d = .{
.indices = &.{0, 1, 2, 0, 2, 3},
.vertices = &.{
.{.xy = .{-half_extent, half_extent}, .uv = .{0, 1}},
.{.xy = .{half_extent, half_extent}, .uv = .{1, 1}},
.{.xy = .{half_extent, -half_extent}, .uv = .{1, 0}},
.{.xy = .{-half_extent, -half_extent}, .uv = .{0, 0}},
},
},
}),
},
.formats = registered_formats, .formats = registered_formats,
.context = context, .context = context,
}); });
@ -239,6 +257,8 @@ pub fn setup(world: *coral.World, events: App.Events) (error {Unsupported} || st
} }
pub fn stop(assets: coral.Write(Assets)) void { pub fn stop(assets: coral.Write(Assets)) void {
assets.res.staging_arena.deinit();
assets.res.formats.deinit();
assets.res.context.deinit(); assets.res.context.deinit();
} }

View File

@ -10,11 +10,6 @@ const sokol = @import("sokol");
const std = @import("std"); const std = @import("std");
pub const Asset = union (enum) {
texture: gfx.Texture,
mesh_2d: gfx.Mesh2D,
};
pub const Context = struct { pub const Context = struct {
window: *ext.SDL_Window, window: *ext.SDL_Window,
thread: std.Thread, thread: std.Thread,
@ -42,6 +37,7 @@ pub const Context = struct {
self.loop.is_running.store(false, .monotonic); self.loop.is_running.store(false, .monotonic);
self.loop.ready.post(); self.loop.ready.post();
self.thread.join(); self.thread.join();
self.loop.deinit();
coral.heap.allocator.destroy(self.loop); coral.heap.allocator.destroy(self.loop);
ext.SDL_DestroyWindow(self.window); ext.SDL_DestroyWindow(self.window);
@ -84,16 +80,20 @@ pub const Context = struct {
}; };
} }
pub fn open(self: *Context, asset: Asset) std.mem.Allocator.Error!gfx.Handle { pub fn open(self: *Context, desc: gfx.Desc) std.mem.Allocator.Error!gfx.Handle {
const open_commands = self.loop.opens.pending(); const open_commands = self.loop.opens.pending();
const index = self.loop.closed_indices.get() orelse open_commands.stack.len(); const index = self.loop.closed_indices.get() orelse open_commands.stack.len();
try open_commands.append(.{ try open_commands.append(.{
.index = index, .index = index,
.payload = asset, .desc = desc,
}); });
try self.loop.closes.pending().stack.grow(1); const pending_closes = self.loop.closes.pending();
if (pending_closes.stack.len() == pending_closes.stack.cap) {
try pending_closes.stack.grow(1);
}
_ = self.loop.closed_indices.pop(); _ = self.loop.closed_indices.pop();
@ -145,13 +145,13 @@ const Loop = struct {
const OpenCommand = struct { const OpenCommand = struct {
index: usize, index: usize,
payload: Asset, desc: gfx.Desc,
fn clone(command: OpenCommand, arena: *std.heap.ArenaAllocator) std.mem.Allocator.Error!OpenCommand { fn clone(command: OpenCommand, arena: *std.heap.ArenaAllocator) std.mem.Allocator.Error!OpenCommand {
const allocator = arena.allocator(); const allocator = arena.allocator();
return .{ return .{
.payload = switch (command.payload) { .desc = switch (command.desc) {
.texture => |texture| .{ .texture => |texture| .{
.texture = .{ .texture = .{
.data = try allocator.dupe(coral.io.Byte, texture.data), .data = try allocator.dupe(coral.io.Byte, texture.data),
@ -163,7 +163,7 @@ const Loop = struct {
.mesh_2d => |mesh_2d| .{ .mesh_2d => |mesh_2d| .{
.mesh_2d = .{ .mesh_2d = .{
.vertices = try allocator.dupe(gfx.Mesh2D.Vertex, mesh_2d.vertices), .vertices = try allocator.dupe(gfx.Desc.Mesh2D.Vertex, mesh_2d.vertices),
.indices = try allocator.dupe(u16, mesh_2d.indices), .indices = try allocator.dupe(u16, mesh_2d.indices),
}, },
}, },
@ -178,6 +178,12 @@ const Loop = struct {
const OpenChain = commands.Chain(OpenCommand, OpenCommand.clone); const OpenChain = commands.Chain(OpenCommand, OpenCommand.clone);
fn deinit(self: *Loop) void {
self.closes.deinit();
self.opens.deinit();
self.closed_indices.deinit();
}
fn run(self: *Loop, window: *ext.SDL_Window) !void { fn run(self: *Loop, window: *ext.SDL_Window) !void {
const context = configure_and_create: { const context = configure_and_create: {
var result = @as(c_int, 0); var result = @as(c_int, 0);
@ -230,7 +236,7 @@ const Loop = struct {
defer open_commands.clear(); defer open_commands.clear();
for (open_commands.stack.values) |command| { for (open_commands.stack.values) |command| {
switch (command.payload) { switch (command.desc) {
.texture => |texture| { .texture => |texture| {
const stride = texture.width * texture.format.byte_size(); const stride = texture.width * texture.format.byte_size();

View File

@ -1,9 +1,69 @@
const coral = @import("coral");
const gfx = @import("../gfx.zig"); const gfx = @import("../gfx.zig");
const std = @import("std"); const std = @import("std");
pub fn open_bmp(path: []const u8) std.mem.Allocator.Error!gfx.Handle { pub fn load_bmp(arena: *std.heap.ArenaAllocator, path: []const u8) gfx.Assets.Format.Error!gfx.Desc {
// TODO: Implement. const header = try coral.files.bundle.read_little(path, 0, extern struct {
_ = path; type: [2]u8 align (1),
unreachable; file_size: u32 align (1),
reserved: [2]u16 align (1),
image_offset: u32 align (1),
header_size: u32 align (1),
pixel_width: i32 align (1),
pixel_height: i32 align (1),
color_planes: u16 align (1),
bits_per_pixel: u16 align (1),
compression_method: u32 align (1),
image_size: u32 align(1),
pixels_per_meter_x: i32 align (1),
pixels_per_meter_y: i32 align (1),
palette_colors_used: u32 align (1),
important_colors_used: u32 align (1),
}) orelse {
return error.Unsupported;
};
if (!std.mem.eql(u8, &header.type, "BM")) {
return error.Unsupported;
}
const pixel_width = std.math.cast(u16, header.pixel_width) orelse {
return error.Unsupported;
};
const pixels = try arena.allocator().alloc(coral.io.Byte, header.image_size);
const bytes_per_pixel = header.bits_per_pixel / coral.io.bits_per_byte;
const alignment = 4;
const byte_stride = pixel_width * bytes_per_pixel;
const padded_byte_stride = alignment * @divTrunc((byte_stride + alignment - 1), alignment);
const byte_padding = coral.scalars.sub(padded_byte_stride, byte_stride) orelse 0;
var buffer_offset: usize = 0;
var file_offset = @as(usize, header.image_offset);
while (buffer_offset < pixels.len) {
const line = pixels[buffer_offset .. buffer_offset + byte_stride];
if (try coral.files.bundle.read_bytes(path, file_offset, line) != byte_stride) {
return error.Unsupported;
}
file_offset = line.len + byte_padding;
buffer_offset += padded_byte_stride;
}
return .{
.texture = .{
.format = switch (header.bits_per_pixel) {
24 => .bgr888,
32 => .bgra8888,
else => return error.Unsupported,
},
.width = pixel_width,
.data = pixels,
.access = .static,
}
};
} }