mirror of
https://codeberg.org/fairyglade/ly.git
synced 2026-08-09 07:09:13 +02:00
perf(DurFile): stream JSON parsing and flatten color maps
This commit is contained in:
parent
a22805d44b
commit
b5c5c9a56f
1 changed files with 354 additions and 51 deletions
|
|
@ -19,25 +19,268 @@ const LogFile = ly_core.LogFile;
|
|||
const enums = @import("../enums.zig");
|
||||
const DurOffsetAlignment = enums.DurOffsetAlignment;
|
||||
|
||||
const JsonTokenSlice = struct {
|
||||
value: []const u8,
|
||||
allocated: ?[]u8 = null,
|
||||
|
||||
fn deinit(self: JsonTokenSlice, allocator: Allocator) void {
|
||||
if (self.allocated) |value| allocator.free(value);
|
||||
}
|
||||
};
|
||||
|
||||
fn freeJsonToken(allocator: Allocator, token: Json.Token) void {
|
||||
switch (token) {
|
||||
.allocated_string, .allocated_number => |value| allocator.free(value),
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn jsonString(allocator: Allocator, reader: *Json.Reader) !JsonTokenSlice {
|
||||
const token = try reader.nextAlloc(allocator, .alloc_if_needed);
|
||||
errdefer freeJsonToken(allocator, token);
|
||||
|
||||
return switch (token) {
|
||||
.string => |value| .{ .value = value },
|
||||
.allocated_string => |value| .{ .value = value, .allocated = value },
|
||||
else => error.UnexpectedToken,
|
||||
};
|
||||
}
|
||||
|
||||
fn jsonOwnedString(allocator: Allocator, reader: *Json.Reader) ![]u8 {
|
||||
const token = try reader.nextAlloc(allocator, .alloc_always);
|
||||
errdefer freeJsonToken(allocator, token);
|
||||
|
||||
return switch (token) {
|
||||
.allocated_string => |value| value,
|
||||
else => error.UnexpectedToken,
|
||||
};
|
||||
}
|
||||
|
||||
fn jsonNumber(allocator: Allocator, reader: *Json.Reader) !JsonTokenSlice {
|
||||
const token = try reader.nextAlloc(allocator, .alloc_if_needed);
|
||||
errdefer freeJsonToken(allocator, token);
|
||||
|
||||
return switch (token) {
|
||||
.number => |value| .{ .value = value },
|
||||
.allocated_number => |value| .{ .value = value, .allocated = value },
|
||||
else => error.UnexpectedToken,
|
||||
};
|
||||
}
|
||||
|
||||
fn jsonI64(allocator: Allocator, reader: *Json.Reader) !i64 {
|
||||
const number = try jsonNumber(allocator, reader);
|
||||
defer number.deinit(allocator);
|
||||
|
||||
if (!Json.isNumberFormattedLikeAnInteger(number.value))
|
||||
return error.InvalidNumber;
|
||||
|
||||
return std.fmt.parseInt(i64, number.value, 10) catch |err| switch (err) {
|
||||
error.Overflow => error.Overflow,
|
||||
error.InvalidCharacter => error.InvalidNumber,
|
||||
};
|
||||
}
|
||||
|
||||
fn jsonI32(allocator: Allocator, reader: *Json.Reader) !i32 {
|
||||
const number = try jsonNumber(allocator, reader);
|
||||
defer number.deinit(allocator);
|
||||
|
||||
if (Json.isNumberFormattedLikeAnInteger(number.value)) {
|
||||
return std.fmt.parseInt(i32, number.value, 10) catch |err| switch (err) {
|
||||
error.Overflow => error.Overflow,
|
||||
error.InvalidCharacter => error.InvalidNumber,
|
||||
};
|
||||
}
|
||||
|
||||
const value = std.fmt.parseFloat(f64, number.value) catch return error.InvalidNumber;
|
||||
if (!std.math.isFinite(value) or @round(value) != value)
|
||||
return error.InvalidNumber;
|
||||
if (value > @as(f64, @floatFromInt(std.math.maxInt(i32))) or
|
||||
value < @as(f64, @floatFromInt(std.math.minInt(i32))))
|
||||
return error.Overflow;
|
||||
|
||||
return @intFromFloat(value);
|
||||
}
|
||||
|
||||
fn jsonFloat(allocator: Allocator, reader: *Json.Reader, allow_integer: bool) !f64 {
|
||||
const number = try jsonNumber(allocator, reader);
|
||||
defer number.deinit(allocator);
|
||||
|
||||
if (!allow_integer and Json.isNumberFormattedLikeAnInteger(number.value))
|
||||
return error.UnexpectedToken;
|
||||
|
||||
const value = std.fmt.parseFloat(f64, number.value) catch return error.InvalidNumber;
|
||||
if (!std.math.isFinite(value)) return error.Overflow;
|
||||
return value;
|
||||
}
|
||||
|
||||
fn jsonF64(allocator: Allocator, reader: *Json.Reader) !f64 {
|
||||
return jsonFloat(allocator, reader, false);
|
||||
}
|
||||
|
||||
fn jsonF32(allocator: Allocator, reader: *Json.Reader) !f32 {
|
||||
return @floatCast(try jsonFloat(allocator, reader, true));
|
||||
}
|
||||
|
||||
const Frame = struct {
|
||||
frameNumber: i32,
|
||||
delay: f32,
|
||||
contents: [][]u8,
|
||||
colorMap: [][][]i32,
|
||||
colorMap: [][2]i32,
|
||||
colorMapHeight: usize,
|
||||
|
||||
// allocator must be outside of struct as it will fail the json parser
|
||||
pub fn deinit(self: *const Frame, allocator: Allocator) void {
|
||||
for (self.contents) |con| {
|
||||
allocator.free(con);
|
||||
}
|
||||
allocator.free(self.contents);
|
||||
fn colorAt(self: *const Frame, x: usize, y: usize) [2]i32 {
|
||||
return self.colorMap[x * self.colorMapHeight + y];
|
||||
}
|
||||
|
||||
for (self.colorMap) |cm| {
|
||||
for (cm) |int2| {
|
||||
allocator.free(int2);
|
||||
fn fromJson(
|
||||
allocator: Allocator,
|
||||
reader: *Json.Reader,
|
||||
expected_columns: ?usize,
|
||||
expected_lines: ?usize,
|
||||
) !Frame {
|
||||
if (.object_begin != try reader.next()) return error.UnexpectedToken;
|
||||
|
||||
var frame_number: ?i32 = null;
|
||||
var delay: ?f32 = null;
|
||||
var contents: ?[][]u8 = null;
|
||||
var color_map: ?[][2]i32 = null;
|
||||
var color_map_height: usize = 0;
|
||||
|
||||
errdefer if (contents) |value| freeContents(allocator, value);
|
||||
errdefer if (color_map) |value| allocator.free(value);
|
||||
|
||||
while (try reader.peekNextTokenType() != .object_end) {
|
||||
const key = try jsonString(allocator, reader);
|
||||
defer key.deinit(allocator);
|
||||
|
||||
if (eql(u8, key.value, "frameNumber")) {
|
||||
if (frame_number != null) return error.DuplicateField;
|
||||
frame_number = try jsonI32(allocator, reader);
|
||||
} else if (eql(u8, key.value, "delay")) {
|
||||
if (delay != null) return error.DuplicateField;
|
||||
delay = try jsonF32(allocator, reader);
|
||||
} else if (eql(u8, key.value, "contents")) {
|
||||
if (contents != null) return error.DuplicateField;
|
||||
contents = try parseContents(allocator, reader);
|
||||
} else if (eql(u8, key.value, "colorMap")) {
|
||||
if (color_map != null) return error.DuplicateField;
|
||||
const parsed = try parseColorMap(
|
||||
allocator,
|
||||
reader,
|
||||
expected_columns,
|
||||
expected_lines,
|
||||
);
|
||||
color_map = parsed.color_map;
|
||||
color_map_height = parsed.height;
|
||||
} else {
|
||||
try reader.skipValue();
|
||||
}
|
||||
allocator.free(cm);
|
||||
}
|
||||
_ = try reader.next();
|
||||
|
||||
return .{
|
||||
.frameNumber = frame_number orelse return error.MissingField,
|
||||
.delay = delay orelse return error.MissingField,
|
||||
.contents = contents orelse return error.MissingField,
|
||||
.colorMap = color_map orelse return error.MissingField,
|
||||
.colorMapHeight = color_map_height,
|
||||
};
|
||||
}
|
||||
|
||||
fn parseContents(allocator: Allocator, reader: *Json.Reader) ![][]u8 {
|
||||
if (.array_begin != try reader.next()) return error.UnexpectedToken;
|
||||
|
||||
var contents: std.ArrayList([]u8) = .empty;
|
||||
errdefer {
|
||||
deinitContents(allocator, contents.items);
|
||||
contents.deinit(allocator);
|
||||
}
|
||||
|
||||
while (try reader.peekNextTokenType() != .array_end) {
|
||||
const content = try jsonOwnedString(allocator, reader);
|
||||
errdefer allocator.free(content);
|
||||
try contents.append(allocator, content);
|
||||
}
|
||||
_ = try reader.next();
|
||||
|
||||
return try contents.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
const ParsedColorMap = struct {
|
||||
color_map: [][2]i32,
|
||||
height: usize,
|
||||
};
|
||||
|
||||
fn parseColorMap(
|
||||
allocator: Allocator,
|
||||
reader: *Json.Reader,
|
||||
expected_columns: ?usize,
|
||||
expected_lines: ?usize,
|
||||
) !ParsedColorMap {
|
||||
if (.array_begin != try reader.next()) return error.UnexpectedToken;
|
||||
|
||||
const expected_len = if (expected_columns != null and expected_lines != null)
|
||||
std.math.mul(usize, expected_columns.?, expected_lines.?) catch return error.Overflow
|
||||
else
|
||||
null;
|
||||
|
||||
var color_map: std.ArrayList([2]i32) = if (expected_len) |len|
|
||||
try .initCapacity(allocator, len)
|
||||
else
|
||||
.empty;
|
||||
errdefer color_map.deinit(allocator);
|
||||
|
||||
var color_map_height: ?usize = null;
|
||||
var column_count: usize = 0;
|
||||
|
||||
while (try reader.peekNextTokenType() != .array_end) {
|
||||
if (.array_begin != try reader.next()) return error.UnexpectedToken;
|
||||
|
||||
var column_height: usize = 0;
|
||||
while (try reader.peekNextTokenType() != .array_end) {
|
||||
if (.array_begin != try reader.next()) return error.UnexpectedToken;
|
||||
|
||||
const foreground = try jsonI32(allocator, reader);
|
||||
const background = try jsonI32(allocator, reader);
|
||||
|
||||
if (.array_end != try reader.next()) return error.LengthMismatch;
|
||||
|
||||
try color_map.append(allocator, .{ foreground, background });
|
||||
column_height += 1;
|
||||
}
|
||||
_ = try reader.next();
|
||||
|
||||
if (color_map_height) |height| {
|
||||
if (column_height != height) return error.LengthMismatch;
|
||||
} else {
|
||||
color_map_height = column_height;
|
||||
}
|
||||
|
||||
column_count += 1;
|
||||
}
|
||||
_ = try reader.next();
|
||||
|
||||
if (expected_columns) |columns| {
|
||||
if (column_count != columns) return error.LengthMismatch;
|
||||
}
|
||||
|
||||
return .{
|
||||
.color_map = try color_map.toOwnedSlice(allocator),
|
||||
.height = color_map_height orelse 0,
|
||||
};
|
||||
}
|
||||
|
||||
fn deinitContents(allocator: Allocator, contents: []const []u8) void {
|
||||
for (contents) |content| allocator.free(content);
|
||||
}
|
||||
|
||||
fn freeContents(allocator: Allocator, contents: [][]u8) void {
|
||||
deinitContents(allocator, contents);
|
||||
allocator.free(contents);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *const Frame, allocator: Allocator) void {
|
||||
freeContents(allocator, self.contents);
|
||||
allocator.free(self.colorMap);
|
||||
}
|
||||
};
|
||||
|
|
@ -51,7 +294,7 @@ const DurFormatRaw = struct {
|
|||
framerate: ?f64 = null,
|
||||
columns: ?i64 = null,
|
||||
lines: ?i64 = null,
|
||||
frames: std.ArrayList(Frame) = undefined,
|
||||
frames: std.ArrayList(Frame) = .empty,
|
||||
|
||||
// Validate data and return a valid DurFormat
|
||||
// Consumes `self`, making it unusable after
|
||||
|
|
@ -81,6 +324,7 @@ const DurFormatRaw = struct {
|
|||
|
||||
if (self.frames.items.len == 0) return error.NoFrames;
|
||||
const frames = self.frames;
|
||||
self.frames = .empty;
|
||||
|
||||
return .{
|
||||
.allocator = self.allocator,
|
||||
|
|
@ -94,51 +338,111 @@ const DurFormatRaw = struct {
|
|||
};
|
||||
}
|
||||
|
||||
fn parseFromJson(self: *DurFormatRaw, allocator: Allocator, dur_json_root: Json.Value) !void {
|
||||
var dur_movie = if (dur_json_root.object.get("DurMovie")) |dm| dm.object else return error.NotValidFile;
|
||||
fn parseFromJson(self: *DurFormatRaw, allocator: Allocator, reader: *Json.Reader) !void {
|
||||
if (.object_begin != try reader.next()) return error.NotValidFile;
|
||||
|
||||
var dur_movie_seen = false;
|
||||
while (try reader.peekNextTokenType() != .object_end) {
|
||||
const key = try jsonString(allocator, reader);
|
||||
defer key.deinit(allocator);
|
||||
|
||||
if (eql(u8, key.value, "DurMovie")) {
|
||||
if (dur_movie_seen) return error.DuplicateField;
|
||||
dur_movie_seen = true;
|
||||
try parseDurMovie(self, allocator, reader);
|
||||
} else {
|
||||
try reader.skipValue();
|
||||
}
|
||||
}
|
||||
_ = try reader.next();
|
||||
|
||||
if (!dur_movie_seen) return error.NotValidFile;
|
||||
if (.end_of_document != try reader.next()) return error.UnexpectedToken;
|
||||
}
|
||||
|
||||
fn parseDurMovie(self: *DurFormatRaw, allocator: Allocator, reader: *Json.Reader) !void {
|
||||
if (.object_begin != try reader.next()) return error.NotValidFile;
|
||||
|
||||
// Depending on the version, a dur file can have different json object names (ie: columns vs sizeX)
|
||||
self.formatVersion = if (dur_movie.get("formatVersion")) |x| x.integer else null;
|
||||
self.colorFormat = if (dur_movie.get("colorFormat")) |x| try allocator.dupe(u8, x.string) else null;
|
||||
self.encoding = if (dur_movie.get("encoding")) |x| try allocator.dupe(u8, x.string) else null;
|
||||
self.framerate = if (dur_movie.get("framerate")) |x| x.float else null;
|
||||
self.columns = if (dur_movie.get("columns")) |x| x.integer else if (dur_movie.get("sizeX")) |x| x.integer else null;
|
||||
var format_version_seen = false;
|
||||
var color_format_seen = false;
|
||||
var encoding_seen = false;
|
||||
var framerate_seen = false;
|
||||
var columns_seen = false;
|
||||
var size_x_seen = false;
|
||||
var lines_seen = false;
|
||||
var size_y_seen = false;
|
||||
var frames_seen = false;
|
||||
|
||||
self.lines = if (dur_movie.get("lines")) |x| x.integer else if (dur_movie.get("sizeY")) |x| x.integer else null;
|
||||
while (try reader.peekNextTokenType() != .object_end) {
|
||||
const key = try jsonString(allocator, reader);
|
||||
defer key.deinit(allocator);
|
||||
|
||||
const frames = dur_movie.get("frames") orelse return error.NotValidFile;
|
||||
|
||||
self.frames = try .initCapacity(allocator, frames.array.items.len);
|
||||
|
||||
for (frames.array.items) |json_frame| {
|
||||
var parsed_frame = try Json.parseFromValue(Frame, allocator, json_frame, .{});
|
||||
defer parsed_frame.deinit();
|
||||
|
||||
const frame_val = parsed_frame.value;
|
||||
|
||||
// copy all fields to own the ptrs for deallocation, the parsed_frame has some other
|
||||
// allocated memory making it difficult to deallocate without leaks
|
||||
const frame: Frame = .{ .frameNumber = frame_val.frameNumber, .delay = frame_val.delay, .contents = try allocator.alloc([]u8, frame_val.contents.len), .colorMap = try allocator.alloc([][]i32, frame_val.colorMap.len) };
|
||||
|
||||
for (0..frame.contents.len) |i| {
|
||||
frame.contents[i] = try allocator.dupe(u8, frame_val.contents[i]);
|
||||
if (eql(u8, key.value, "formatVersion")) {
|
||||
if (format_version_seen) return error.DuplicateField;
|
||||
format_version_seen = true;
|
||||
self.formatVersion = try jsonI64(allocator, reader);
|
||||
} else if (eql(u8, key.value, "colorFormat")) {
|
||||
if (color_format_seen) return error.DuplicateField;
|
||||
color_format_seen = true;
|
||||
self.colorFormat = try jsonOwnedString(allocator, reader);
|
||||
} else if (eql(u8, key.value, "encoding")) {
|
||||
if (encoding_seen) return error.DuplicateField;
|
||||
encoding_seen = true;
|
||||
self.encoding = try jsonOwnedString(allocator, reader);
|
||||
} else if (eql(u8, key.value, "framerate")) {
|
||||
if (framerate_seen) return error.DuplicateField;
|
||||
framerate_seen = true;
|
||||
self.framerate = try jsonF64(allocator, reader);
|
||||
} else if (eql(u8, key.value, "columns")) {
|
||||
if (columns_seen) return error.DuplicateField;
|
||||
columns_seen = true;
|
||||
self.columns = try jsonI64(allocator, reader);
|
||||
} else if (eql(u8, key.value, "sizeX")) {
|
||||
if (size_x_seen) return error.DuplicateField;
|
||||
size_x_seen = true;
|
||||
const size_x = try jsonI64(allocator, reader);
|
||||
if (!columns_seen) self.columns = size_x;
|
||||
} else if (eql(u8, key.value, "lines")) {
|
||||
if (lines_seen) return error.DuplicateField;
|
||||
lines_seen = true;
|
||||
self.lines = try jsonI64(allocator, reader);
|
||||
} else if (eql(u8, key.value, "sizeY")) {
|
||||
if (size_y_seen) return error.DuplicateField;
|
||||
size_y_seen = true;
|
||||
const size_y = try jsonI64(allocator, reader);
|
||||
if (!lines_seen) self.lines = size_y;
|
||||
} else if (eql(u8, key.value, "frames")) {
|
||||
if (frames_seen) return error.DuplicateField;
|
||||
frames_seen = true;
|
||||
try parseFrames(self, allocator, reader);
|
||||
} else {
|
||||
try reader.skipValue();
|
||||
}
|
||||
}
|
||||
_ = try reader.next();
|
||||
|
||||
// colorMap is stored as an 3d array where:
|
||||
// the outer (i) most array is the horizontal position of the color
|
||||
// the middle (j) is the vertical position of the color
|
||||
// the inner (0/1) is the foreground/background color
|
||||
for (0..frame.colorMap.len) |i| {
|
||||
frame.colorMap[i] = try allocator.alloc([]i32, frame_val.colorMap[i].len);
|
||||
for (0..frame.colorMap[i].len) |j| {
|
||||
frame.colorMap[i][j] = try allocator.alloc(i32, 2);
|
||||
frame.colorMap[i][j][0] = frame_val.colorMap[i][j][0];
|
||||
frame.colorMap[i][j][1] = frame_val.colorMap[i][j][1];
|
||||
}
|
||||
}
|
||||
if (!frames_seen) return error.NotValidFile;
|
||||
}
|
||||
|
||||
fn parseFrames(self: *DurFormatRaw, allocator: Allocator, reader: *Json.Reader) !void {
|
||||
if (.array_begin != try reader.next()) return error.UnexpectedToken;
|
||||
|
||||
const expected_columns = if (self.columns) |columns|
|
||||
std.math.cast(usize, columns)
|
||||
else
|
||||
null;
|
||||
const expected_lines = if (self.lines) |lines|
|
||||
std.math.cast(usize, lines)
|
||||
else
|
||||
null;
|
||||
|
||||
while (try reader.peekNextTokenType() != .array_end) {
|
||||
var frame = try Frame.fromJson(allocator, reader, expected_columns, expected_lines);
|
||||
errdefer frame.deinit(allocator);
|
||||
try self.frames.append(allocator, frame);
|
||||
}
|
||||
_ = try reader.next();
|
||||
}
|
||||
|
||||
pub fn createFromFile(self: *DurFormatRaw, allocator: Allocator, io: std.Io, file_path: []const u8) !void {
|
||||
|
|
@ -154,10 +458,7 @@ const DurFormatRaw = struct {
|
|||
var json_reader = Json.Reader.init(allocator, &decompress.reader);
|
||||
defer json_reader.deinit();
|
||||
|
||||
const json = try Json.parseFromTokenSource(Json.Value, allocator, &json_reader, .{});
|
||||
defer json.deinit();
|
||||
|
||||
try parseFromJson(self, allocator, json.value);
|
||||
try parseFromJson(self, allocator, &json_reader);
|
||||
}
|
||||
|
||||
pub fn init(allocator: Allocator) DurFormatRaw {
|
||||
|
|
@ -167,6 +468,8 @@ const DurFormatRaw = struct {
|
|||
pub fn deinit(self: *DurFormatRaw) void {
|
||||
if (self.colorFormat) |str| self.allocator.free(str);
|
||||
if (self.encoding) |str| self.allocator.free(str);
|
||||
for (self.frames.items) |frame| frame.deinit(self.allocator);
|
||||
self.frames.deinit(self.allocator);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -520,7 +823,7 @@ fn draw(self: *DurFile) void {
|
|||
const cell_x = @as(i32, @intCast(x)) + self.start_pos[VEC_X];
|
||||
|
||||
const codepoint: u21 = iter.nextCodepoint().?;
|
||||
const color_map = current_frame.colorMap[x][y];
|
||||
const color_map = current_frame.colorAt(x, y);
|
||||
|
||||
var color_map_0: u32 = @intCast(if (color_map[0] == -1) 0 else color_map[0]);
|
||||
var color_map_1: u32 = @intCast(if (color_map[1] == -1) 0 else color_map[1]);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue