const std = @import("std");
const sorvi = @import("sorvi");
pub const std_options: std.Options = .{
.logFn = sorvi.defaultLog,
.queryPageSize = sorvi.queryPageSize,
.page_size_max = sorvi.page_size_max,
.log_level = .debug,
};
pub const os = sorvi.os;
pub const panic = std.debug.FullPanic(sorvi.defaultPanic);
comptime {
sorvi.init(@This(), .{
.id = "org.sorvi.test.core.video_v1",
.name = "test.core.video_v1",
.version = "0.0.0",
.core_extensions = &.{ .core_v1, .video_v1 },
.frontend_extensions = &.{ .core_v1, .raster_v1 },
});
}
ticks: usize = 0,
accumulated_error_ns: i64 = 0,
max_error_ns: i64 = std.math.minInt(i64),
min_error_ns: i64 = std.math.maxInt(i64),
prev_error_ns: i64 = 0,
max_delta_error_ns: u64 = 0,
bad_short_frames: usize = 0,
pub fn init(_: *@This()) !void {
std.debug.assert(sorvi.video_v1.current_display_mode().id == .default);
var num_defaults: usize = 0;
for (sorvi.video_v1.query_display_modes()) |mode| {
num_defaults += @intFromBool(mode.id == .default);
std.debug.assert(mode.scale > 0.0);
std.debug.assert(mode.w > 0);
std.debug.assert(mode.h > 0);
}
std.debug.assert(num_defaults == 1);
try sorvi.raster_v1.init(.{
.format = .xrgb8888,
.scaling = null,
.direct = false,
});
try sorvi.video_v1.configure(.{
.w = 1,
.h = 1,
.flags = .{ .border = false },
.presentation = .fixed,
.mode = .default,
});
}
pub fn deinit(self: *@This()) void {
const mean_error_ns = @divFloor(self.accumulated_error_ns, total_runs);
const mean_bad = @abs(mean_error_ns) > std.time.ns_per_ms;
const max_bad = @abs(self.max_error_ns) > std.time.ns_per_ms * 2;
const min_bad = @abs(self.min_error_ns) > std.time.ns_per_ms * 2;
const rel_bad = self.bad_short_frames > total_runs / 5;
const delta_bad = self.max_delta_error_ns > std.time.ns_per_ms;
if (mean_bad or max_bad or min_bad or rel_bad) {
std.log.info("sorvi.test.suite: failed frontend timing tests", .{});
} else {
std.log.info("sorvi.test.suite: passed frontend timing tests", .{});
}
std.log.info("mean signed error: {}ns ({s})", .{ mean_error_ns, if (mean_bad) "BAD" else "OK" });
std.log.info("max late: {}ns ({s})", .{ self.max_error_ns, if (max_bad) "BAD" else "OK" });
std.log.info("max early: {}ns ({s})", .{ self.min_error_ns, if (min_bad) "BAD" else "OK" });
std.log.info("bad short frames: {} ({s})", .{ self.bad_short_frames, if (rel_bad) "BAD" else "OK" });
std.log.info("max pacing delta: {}ns ({s})", .{ self.max_delta_error_ns, if (delta_bad) "BAD" else "OK" });
}
const total_runs: usize = 128;
const targets: []const i64 = &.{
std.time.ns_per_ms * 16,
std.time.ns_per_ms * 33,
std.time.ns_per_ms * 23,
std.time.ns_per_ms * 8,
std.time.ns_per_ms * 7,
};
pub fn videoTick(self: *@This(), frame: sorvi.video_v1.frame_t) !u64 {
std.debug.assert(frame.w == 1 and frame.h == 1);
if (self.ticks > 0) {
const idx = self.ticks % targets.len;
const error_ns: i64 = @as(i64, @intCast(frame.time_ns)) - targets[idx];
self.accumulated_error_ns += error_ns;
self.max_error_ns = @max(error_ns, self.max_error_ns);
self.min_error_ns = @min(error_ns, self.min_error_ns);
const rel = @as(f64, @floatFromInt(@abs(error_ns))) / @as(f64, @floatFromInt(targets[idx]));
if (targets[idx] < 10 * std.time.ns_per_ms and @abs(error_ns) > 500_000 and rel > 0.10) {
self.bad_short_frames += 1;
}
const delta = error_ns - self.prev_error_ns;
self.max_delta_error_ns = @max(self.max_delta_error_ns, @abs(delta));
self.prev_error_ns = error_ns;
}
if (self.ticks >= total_runs) {
sorvi.core_v1.exit();
}
self.ticks += 1;
const idx = self.ticks % targets.len;
return @intCast(targets[idx]);
}