Skip to content

Commit

Permalink
Rework video rendering mechanism to be cross platform
Browse files Browse the repository at this point in the history
  • Loading branch information
adamws committed Nov 25, 2024
1 parent 878c0c4 commit 8402248
Show file tree
Hide file tree
Showing 10 changed files with 191 additions and 163 deletions.
44 changes: 36 additions & 8 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,36 +51,60 @@ jobs:
name: Run functional tests
needs:
- build-and-test
runs-on: ubuntu-latest
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: klawa-ubuntu-latest
name: klawa-${{ matrix.os }}
path: zig-out/bin
- name: Install dependencies
shell: bash
run: |
pwd
ls -alh zig-out/bin
- if: matrix.os == 'ubuntu-latest'
name: Install dependencies
shell: bash
run: |
sudo apt-get update
sudo apt-get install ffmpeg xdotool x11-apps xvfb
- if: matrix.os == 'windows-latest'
name: Install dependencies
shell: bash
run: |
choco install autohotkey.portable ffmpeg
- if: matrix.os == 'windows-latest'
name: Install Mesa
shell: cmd
run: |
curl.exe -L --output mesa.7z --url https://github.com/pal1000/mesa-dist-win/releases/download/24.2.7/mesa3d-24.2.7-release-msvc.7z
"C:\Program Files\7-Zip\7z.exe" x mesa.7z
dir
systemwidedeploy.cmd 1
- name: Install python dependencies
shell: bash
run: |
cd tests
python -m venv .env
. .env/bin/activate
pip install -r dev-requirements.txt
- name: Run tests
- if: matrix.os == 'ubuntu-latest'
name: Fix executable permisions
shell: bash
run: |
chmod +x zig-out/bin/klawa
- name: Run tests
shell: bash
run: |
# not running with pytest-xdist because renders are way off
# when framerate drops below expected 60fps:
cd tests && . .env/bin/activate && python -m pytest src/
cd tests && python -m pytest src/
- uses: actions/upload-artifact@v4
if: always()
with:
name: report
name: report-${{ matrix.os }}
path: tests/report/
retention-days: 2
if-no-files-found: error
Expand All @@ -90,6 +114,9 @@ jobs:
needs:
- run-functional-tests
runs-on: ubuntu-latest
strategy:
matrix:
tested-os: [ubuntu-latest, windows-latest]
defaults:
run:
shell: bash
Expand All @@ -98,7 +125,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: report
name: report-${{ matrix.tested-os }}
path: tests/report
- name: Install Vercel CLI
run: npm install --global vercel@latest
Expand All @@ -112,4 +139,5 @@ jobs:
shell: bash
run: |
echo '### Deployed' >> $GITHUB_STEP_SUMMARY
echo ${{ matrix.tested-os }} >> $GITHUB_STEP_SUMMARY
cat url.txt >> $GITHUB_STEP_SUMMARY
9 changes: 8 additions & 1 deletion src/ffmpeg.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ const process = std.process;
const Child = process.Child;
const File = std.fs.File;

const builtin = @import("builtin");

pub const Ffmpeg = struct {
child: Child,

Expand All @@ -14,8 +16,13 @@ pub const Ffmpeg = struct {
) !Ffmpeg {
const resolution = try std.fmt.allocPrint(allocator, "{d}x{d}", .{ width, height });
defer allocator.free(resolution);
const exec_name: []const u8 = switch (builtin.target.os.tag) {
.linux => "ffmpeg",
.windows => "ffmpeg.exe",
else => @compileError("unsupported platform"),
};
const args = [_][]const u8{
"ffmpeg", "-y",
exec_name, "-y",
"-f", "rawvideo",
"-framerate", "60",
"-s", resolution,
Expand Down
114 changes: 99 additions & 15 deletions src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ const KeyOnScreen = struct {
pressed: bool,
};

pub const KeyData = struct {
pub const KeyData = extern struct {
pressed: bool,
repeated: bool,
keycode: u8,
Expand Down Expand Up @@ -151,6 +151,49 @@ const CodepointBuffer = struct {
}
};

const KeyDataProducer = struct {
reader: std.io.AnyReader,
frame_read: bool,
next_frame: usize = 0,

pub fn init(reader: std.io.AnyReader) KeyDataProducer {
return .{
.reader = reader,
.frame_read = true,
};
}

fn nextFrame(self: *KeyDataProducer) !usize {
if (self.frame_read) {
self.next_frame = try self.reader.readInt(usize, .little);
self.frame_read = false;
}
return self.next_frame;
}

fn next(self: *KeyDataProducer) !KeyData {
const key_data = try self.reader.readStruct(KeyData);
self.frame_read = true;
return key_data;
}

pub fn getDataForFrame(self: *KeyDataProducer, frame: usize) !?KeyData {
const next_frame = try self.nextFrame();
if (next_frame == frame) {
return try self.next();
}
return null;
}
};

fn getDataForFrame(frame: usize) !?KeyData {
if (key_data_producer) |*producer| {
return try producer.getDataForFrame(frame);
}
return null;
}

var key_data_producer: ?KeyDataProducer = null;
pub var app_state: AppState = undefined;

// https://github.com/bits/UTF-8-Unicode-Test-Documents/blob/master/UTF-8_sequence_unseparated/utf8_sequence_0-0xfff_assigned_printable_unseparated.txt
Expand Down Expand Up @@ -595,18 +638,18 @@ pub fn main() !void {
}
defer if (pixels) |p| allocator.free(p);

var thread: ?std.Thread = null;
if (res.args.replay) |replay_file| {
// TODO: this will start processing events before rendering ready, add synchronization
const loop = res.args.@"replay-loop" != 0;
thread = try std.Thread.spawn(.{}, backend.producer, .{ &app_state, window_handle, replay_file, loop });
} else {
// TODO: assign to thread var when close supported, join on this thread won't work now
_ = try std.Thread.spawn(.{}, backend.listener, .{ &app_state, window_handle, res.args.record });
const file = try fs.cwd().openFile(replay_file, .{});
//defer file.close();
var buf_reader = std.io.bufferedReader(file.reader());
const reader = buf_reader.reader();
key_data_producer = KeyDataProducer.init(reader.any());
}

if (res.args.replay == null) {
// TODO: defer when close supported, join on this thread won't work now
_ = try std.Thread.spawn(.{}, backend.listener, .{ &app_state, window_handle });
}
defer if (thread) |t| {
t.join();
};

var keycap_texture = blk: {
const theme = Theme.fromString(app_config.data.theme) orelse unreachable;
Expand Down Expand Up @@ -664,6 +707,17 @@ pub fn main() !void {

var drag_reference_position = rl.getWindowPosition();

// TODO: use buffered writer, to do that we must gracefully handle this thread exit,
// otherwise there is no good place to ensure writer flush
// TODO: support full file path
var event_file: ?fs.File = null;
if (res.args.record) |record_file| {
event_file = try cwd.createFile(record_file, .{});
}
defer if(event_file) |value| value.close();

var frame: usize = 0;

while (!exit_window) {
if (rl.windowShouldClose()) {
exit_window = true;
Expand Down Expand Up @@ -749,7 +803,36 @@ pub fn main() !void {
};
}

// replay
if (getDataForFrame(frame)) |data| {
if (data) |value| {
if (value.string[0] != 0) {
// update only for keys which produe output,
// this will not include modifiers
app_state.last_char_timestamp = std.time.timestamp();
}
std.debug.print("Push {any}\n", .{value});
_ = app_state.keys.push(value);
}
} else |err| switch (err) {
error.EndOfStream => {
std.debug.print("END OF STREAM\n", .{});
exit_window = true;
},
else => return err,
}

if (app_state.keys.pop()) |k| {
// save state (if recording)
if (event_file) |value| {
_ = try value.writeAll(std.mem.asBytes(&frame));
// TODO:
// writing full k struct is very wasteful, there is a lot of non-essential
// data (for example each release event writes 32 bytes of empty string),
// do not worry about that now, optimize for space later.
_ = try value.writeAll(std.mem.asBytes(&k));
}

app_state.updateKeyStates(@intCast(k.keycode), k.pressed);

const symbol = backend.keysymToString(k.keysym);
Expand Down Expand Up @@ -833,6 +916,7 @@ pub fn main() !void {
}
rl.endDrawing();
tracy.frameMark();
frame += 1;

if (renderer) |*r| {
const rendering = tracy.traceNamed(@src(), "render");
Expand All @@ -852,14 +936,14 @@ pub fn main() !void {
);

try r.write(pixels.?);

if (!backend.is_running) {
exit_window = true;
}
}
}

// NOTE: not able to stop x11Listener yet, applicable only for x11Producer
switch (builtin.target.os.tag) {
.windows => backend.stop(),
else => {},
}
backend.is_running = false;

if (renderer) |*r| try r.wait();
Expand Down
1 change: 0 additions & 1 deletion src/textures.zig
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ fn getPositions(sizes: []const rl.Vector2) [keycap_sizes.len]rl.Vector2 {
}

pub fn getPositionBySize(size: rl.Vector2) rl.Vector2 {
std.debug.print("Looking for {d} {d}\n", .{size.x, size.y});
for (keycap_sizes, 0..) |s, i| {
if (size.x == s.x and size.y == s.y) {
return atlas_positions[i];
Expand Down
22 changes: 10 additions & 12 deletions src/win32.zig
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const labels_lookup = @import("layout_labels.zig").labels_lookup;

pub var is_running: bool = false;

var thread_id: c.DWORD = undefined;
var app_state_l: *AppState = undefined;
var layout: c.HKL = undefined;
var hook: ?c.HHOOK = null;
Expand Down Expand Up @@ -73,12 +74,12 @@ fn lowLevelKeyboardProc(nCode: c.INT, wParam: c.WPARAM, lParam: c.LPARAM) callco
return c.CallNextHookEx(hook.?, nCode, wParam, lParam);
}

pub fn listener(app_state: *AppState, window_handle: *anyopaque, record_file: ?[]const u8) !void {
pub fn listener(app_state: *AppState, window_handle: *anyopaque) !void {
defer is_running = false; // stopping not implemented yet
is_running = true;

_ = window_handle;
_ = record_file;
thread_id = c.GetCurrentThreadId();

app_state_l = app_state;
layout = c.GetKeyboardLayout(0);
Expand All @@ -87,23 +88,20 @@ pub fn listener(app_state: *AppState, window_handle: *anyopaque, record_file: ?[
defer _ = c.UnhookWindowsHookEx(hook.?);

var msg: c.MSG = undefined;
while (c.GetMessageA(&msg, null, 0, 0) > 0) {
while (is_running and c.GetMessageA(&msg, null, 0, 0) > 0) {
_ = c.TranslateMessage(&msg);
_ = c.DispatchMessageA(&msg);
}
std.debug.print("Exit win32 listener\n", .{});
}

pub fn keysymToString(keysym: c_ulong) [*c]const u8 {
return kbd_en_vscname[@as(usize, @intCast(keysym))].ptr;
pub fn stop() void {
is_running = false;
_ = c.PostThreadMessageA(thread_id, c.WM_QUIT, 0, 0);
}

// uses events stored in file to reproduce them
// assumes that only expected event types are recorded
pub fn producer(app_state: *AppState, window_handle: *anyopaque, replay_file: []const u8, loop: bool) !void {
_ = app_state;
_ = window_handle;
_ = replay_file;
_ = loop;
pub fn keysymToString(keysym: c_ulong) [*c]const u8 {
return kbd_en_vscname[@as(usize, @intCast(keysym))].ptr;
}

// must match names used by X11:
Expand Down
Loading

0 comments on commit 8402248

Please sign in to comment.