const c = @import("c.zig"); const gl = @import("gl.zig"); const std = @import("std"); const cfg = @import("config.zig"); const build_config = @import("build_config"); fn verify_args(expected: u8, got: []const u8) !void { for (got) |typ| { if (typ != expected) { std.debug.print("expected '{c}' but got '{c}' element (expected {s})\n", .{ expected, typ, got }); return error.typeMismatch; } } } fn set_one( comptime T: type, dest: *T, argv: []const [*c]c.lo_arg, types: []const u8, ) !void { if (types.len != 1) return error.sizeMismatch; switch (T) { f32 => { try verify_args('f', types); dest.* = argv[0].*.f; }, f64 => { try verify_args('d', types); dest.* = argv[0].*.d; }, i32 => { try verify_args('d', types); dest.* = argv[0].*.i; }, u32 => { try verify_args('d', types); const val = argv[0].*.i; if (val < 0) return error.signDisallowed; dest.* = @as(u32, @intCast(val)); }, else => return error.invalidType, } } fn set_array( comptime T: type, dest: []T, argv: []const [*c]c.lo_arg, types: []const u8, ) !void { if (types.len != dest.len) return error.sizeMismatch; switch (T) { f32 => { try verify_args('f', types); for (dest, 0..) |*v, i| v.* = argv[i].*.f; }, f64 => { try verify_args('d', types); for (dest, 0..) |*v, i| v.* = argv[i].*.d; }, i32 => { try verify_args('i', types); for (dest, 0..) |*v, i| v.* = argv[i].*.i; }, u32 => { try verify_args('i', types); for (dest, 0..) |*v, i| { const val = argv[i].*.i; if (val < 0) return error.signDisallowed; v.* = @as(u32, @intCast(argv[i].*.i)); } }, else => return error.invalidType, } } fn set_texture( cache: *gl.UniformCache, dest: *?*gl.Texture, texture_type: gl.Texture.Type, types: []const u8, argv: []const [*c]c.lo_arg, ) !void { if (types.len != 1 or types[0] != 's') return error.invalidType; const nameZ: [*:0]const u8 = @ptrCast(&argv[0].*.s); const name = std.mem.span(nameZ); const source = cache.textures.get(name) orelse return error.texNotFound; if (source.texture.type != texture_type) return error.wrongTextureType; dest.* = &source.texture; } pub const ControlServer = struct { server: c.lo_server, cache: *gl.UniformCache, config: *cfg.Config, progress: std.Progress.Node, allocator: std.mem.Allocator, const ZigCallback = fn (*ControlServer, []const u8, []const u8, []const [*c]c.lo_arg) anyerror!bool; const CCallback = fn ([*c]const u8, [*c]const u8, [*c][*c]c.lo_arg, c_int, c.lo_message, ?*anyopaque) callconv(.C) c_int; fn wrapCallback(comptime inner: ZigCallback) CCallback { return struct { fn wrapped( _path: ?[*:0]const u8, _types: ?[*:0]const u8, _argv: [*c][*c]c.lo_arg, argc: c_int, msg: c.lo_message, userdata: ?*anyopaque, ) callconv(.C) c_int { const self: *ControlServer = @ptrCast(@alignCast(userdata.?)); const path = std.mem.span(_path.?); const types = std.mem.span(_types.?); const args = if (_argv) |argv| argv[0..@intCast(argc)] else &.{}; _ = msg; const res = inner(self, path, types, args) catch |err| { std.debug.print("Error handling {s}: {}\n", .{ path, err }); return 1; }; return if (res) 0 else 1; } }.wrapped; } pub fn init( allocator: std.mem.Allocator, progress: std.Progress.Node, config: *cfg.Config, cache: *gl.UniformCache, ) !*ControlServer { var self: *ControlServer = try allocator.create(ControlServer); self.* = .{ .server = null, .cache = cache, .config = config, .progress = progress, .allocator = allocator, }; self.server = c.lo_server_new_from_url(config.osc.ptr, handle_error); const url: [*:0]const u8 = c.lo_server_get_url(self.server); std.debug.print("listening for OSC messages at {s}\n", .{url}); if (self.server == null) return error.serverInitializationError; _ = c.lo_server_add_method(self.server, "/shader", "s", wrapCallback(handle_shader), @as(*anyopaque, @ptrCast(self))); // _ = c.lo_server_add_method(self.server, "/reload", "", handle_reload, @as(*anyopaque, @ptrCast(self))); _ = c.lo_server_add_method(self.server, "/uniform/*", null, wrapCallback(handle_uniform), @as(*anyopaque, @ptrCast(self))); if (build_config.have_ffmpeg) { _ = c.lo_server_add_method(self.server, "/texture/*/video", "ss", wrapCallback(handle_texture_video), @as(*anyopaque, @ptrCast(self))); _ = c.lo_server_add_method(self.server, "/texture/*/stream", "ss", wrapCallback(handle_texture_stream), @as(*anyopaque, @ptrCast(self))); } if (build_config.have_tsv) { _ = c.lo_server_add_method(self.server, "/texture/*/tsv", "ss", wrapCallback(handle_texture_tsv), @as(*anyopaque, @ptrCast(self))); } _ = c.lo_server_add_method(self.server, "/texture/*/destroy", "", wrapCallback(handle_destroy_texture), @as(*anyopaque, @ptrCast(self))); return self; } pub fn update(self: *ControlServer) void { while (c.lo_server_recv_noblock(self.server, 0) > 0) {} } pub fn destroy(self: ControlServer) void { c.lo_server_free(self.server); } fn handle_error(num: c_int, msg: [*c]const u8, where: [*c]const u8) callconv(.C) void { std.debug.print( "OSC error {} @ {?s}: {?s}\n", .{ num, @as(?[*:0]const u8, @ptrCast(where)), @as(?[*:0]const u8, @ptrCast(msg)) }, ); } fn handle_shader(self: *ControlServer, path: []const u8, types: []const u8, argv: []const [*c]c.lo_arg) !bool { _ = path; _ = types; const code: [*:0]const u8 = @ptrCast(&argv[0].*.s); try self.cache.shader.loadString(std.mem.span(code)); try self.cache.refresh(); std.debug.print("reloaded\n", .{}); return true; } fn handle_reload(self: *ControlServer, path: []const u8, types: []const u8, argv: []const [*c]c.lo_arg) !bool { _ = path; _ = types; _ = argv; try self.cache.shader.loadFile(self.config.fragment, 2048); try self.cache.refresh(); return true; } fn handle_uniform(self: *ControlServer, path: []const u8, types: []const u8, argv: []const [*c]c.lo_arg) !bool { var parts = std.mem.tokenizeScalar(u8, path, '/'); _ = parts.next(); // skip /uniform const uniform = blk: { const uniform_name = parts.next() orelse return error.invalidMessage; const nameZ = try self.allocator.dupeZ(u8, uniform_name); defer self.allocator.free(nameZ); break :blk try self.cache.get(nameZ) orelse return false; }; var value = (uniform.value orelse unreachable).getPointer(); while (parts.next()) |component| { const i = switch (component[0]) { 'x', 'r', 's' => 0, 'y', 'g', 't' => 1, 'z', 'b', 'p' => 2, 'w', 'a', 'q' => 3, else => std.fmt.parseUnsigned(u8, component, 10) catch null, }; if (i) |ii| { value = try value.index(ii); } else { return error.invalidIndex; } } try switch (value) { .FLOAT => |val| set_one(f32, val, argv, types), .DOUBLE => |val| set_one(f64, val, argv, types), .INT => |val| set_one(i32, val, argv, types), .UNSIGNED_INT => |val| set_one(u32, val, argv, types), .BOOL => |val| set_one(u32, val, argv, types), .FLOAT_VEC2 => |val| set_array(f32, val.*[0..], argv, types), .FLOAT_VEC3 => |val| set_array(f32, val.*[0..], argv, types), .FLOAT_VEC4 => |val| set_array(f32, val.*[0..], argv, types), .DOUBLE_VEC2 => |val| set_array(f64, val.*[0..], argv, types), .DOUBLE_VEC3 => |val| set_array(f64, val.*[0..], argv, types), .DOUBLE_VEC4 => |val| set_array(f64, val.*[0..], argv, types), .INT_VEC2 => |val| set_array(i32, val.*[0..], argv, types), .INT_VEC3 => |val| set_array(i32, val.*[0..], argv, types), .INT_VEC4 => |val| set_array(i32, val.*[0..], argv, types), .UNSIGNED_INT_VEC2 => |val| set_array(u32, val.*[0..], argv, types), .UNSIGNED_INT_VEC3 => |val| set_array(u32, val.*[0..], argv, types), .UNSIGNED_INT_VEC4 => |val| set_array(u32, val.*[0..], argv, types), .BOOL_VEC2 => |val| set_array(u32, val.*[0..], argv, types), .BOOL_VEC3 => |val| set_array(u32, val.*[0..], argv, types), .BOOL_VEC4 => |val| set_array(u32, val.*[0..], argv, types), .FLOAT_MAT2 => |val| set_array(f32, val.*[0..], argv, types), .FLOAT_MAT3 => |val| set_array(f32, val.*[0..], argv, types), .FLOAT_MAT4 => |val| set_array(f32, val.*[0..], argv, types), .FLOAT_MAT2x3 => |val| set_array(f32, val.*[0..], argv, types), .FLOAT_MAT2x4 => |val| set_array(f32, val.*[0..], argv, types), .FLOAT_MAT3x2 => |val| set_array(f32, val.*[0..], argv, types), .FLOAT_MAT3x4 => |val| set_array(f32, val.*[0..], argv, types), .FLOAT_MAT4x2 => |val| set_array(f32, val.*[0..], argv, types), .FLOAT_MAT4x3 => |val| set_array(f32, val.*[0..], argv, types), .DOUBLE_MAT2 => |val| set_array(f64, val.*[0..], argv, types), .DOUBLE_MAT3 => |val| set_array(f64, val.*[0..], argv, types), .DOUBLE_MAT4 => |val| set_array(f64, val.*[0..], argv, types), .DOUBLE_MAT2x3 => |val| set_array(f64, val.*[0..], argv, types), .DOUBLE_MAT2x4 => |val| set_array(f64, val.*[0..], argv, types), .DOUBLE_MAT3x2 => |val| set_array(f64, val.*[0..], argv, types), .DOUBLE_MAT3x4 => |val| set_array(f64, val.*[0..], argv, types), .DOUBLE_MAT4x2 => |val| set_array(f64, val.*[0..], argv, types), .DOUBLE_MAT4x3 => |val| set_array(f64, val.*[0..], argv, types), .SAMPLER_2D, .SAMPLER_2D_SHADOW, .INT_SAMPLER_2D, .UNSIGNED_INT_SAMPLER_2D, => |val| set_texture(self.cache, val, .TEXTURE_2D, types, argv), .SAMPLER_2D_ARRAY, .SAMPLER_2D_ARRAY_SHADOW, .INT_SAMPLER_2D_ARRAY, .UNSIGNED_INT_SAMPLER_2D_ARRAY, => |val| set_texture(self.cache, val, .TEXTURE_2D_ARRAY, types, argv), .SAMPLER_3D, .INT_SAMPLER_3D, .UNSIGNED_INT_SAMPLER_3D, => |val| set_texture(self.cache, val, .TEXTURE_3D, types, argv), else => error.uniformNotSupported, }; uniform.setShaderValue(self.cache.shader.*); return true; } fn handle_texture_video(self: *ControlServer, path: []const u8, types: []const u8, argv: []const [*c]c.lo_arg) !bool { _ = types; var parts = std.mem.tokenizeScalar(u8, path, '/'); _ = parts.next(); const name = parts.next() orelse unreachable; if (self.cache.textures.contains(name)) return error.textureNameTaken; const texture_typeZ: [*:0]const u8 = @ptrCast(&argv[0].*.s); const texture_type = std.meta.stringToEnum(gl.Texture.Type, std.mem.span(texture_typeZ)) orelse return error.invalidType; const filenameZ: [*:0]const u8 = @ptrCast(&argv[1].*.s); const ffmpeg = @import("ffmpeg.zig"); const source = try ffmpeg.VideoSource.init(self.cache.allocator, self.progress, filenameZ, texture_type); const key = try self.cache.allocator.dupe(u8, name); errdefer self.cache.allocator.free(key); try self.cache.textures.put(key, source); return true; } fn handle_texture_stream(self: *ControlServer, path: []const u8, types: []const u8, argv: []const [*c]c.lo_arg) !bool { _ = types; var parts = std.mem.tokenizeScalar(u8, path, '/'); _ = parts.next(); const name = parts.next() orelse unreachable; if (self.cache.textures.contains(name)) return error.textureNameTaken; const texture_typeZ: [*:0]const u8 = @ptrCast(&argv[0].*.s); const texture_type = std.meta.stringToEnum(gl.Texture.Type, std.mem.span(texture_typeZ)) orelse return error.invalidType; const filenameZ: [*:0]const u8 = @ptrCast(&argv[1].*.s); const ffmpeg = @import("ffmpeg.zig"); const source = try ffmpeg.StreamSource.init(self.cache.allocator, self.progress, filenameZ, texture_type); const key = try self.cache.allocator.dupe(u8, name); errdefer self.cache.allocator.free(key); try self.cache.textures.put(key, source); return true; } fn handle_texture_tsv(self: *ControlServer, path: []const u8, types: []const u8, argv: []const [*c]c.lo_arg) !bool { _ = types; var parts = std.mem.tokenizeScalar(u8, path, '/'); _ = parts.next(); const name = parts.next() orelse unreachable; if (self.cache.textures.contains(name)) return error.textureNameTaken; const texture_typeZ: [*:0]const u8 = @ptrCast(&argv[0].*.s); const texture_type = std.meta.stringToEnum(gl.Texture.Type, std.mem.span(texture_typeZ)) orelse return error.invalidType; const tsv_nameZ: [*:0]const u8 = @ptrCast(&argv[1].*.s); const tsv = @import("tsv.zig"); const source = try tsv.TSVSource.init(self.cache.allocator, tsv_nameZ, texture_type); const key = try self.cache.allocator.dupe(u8, name); errdefer self.cache.allocator.free(key); try self.cache.textures.put(key, source); return true; } fn handle_destroy_texture(self: *ControlServer, path: []const u8, types: []const u8, argv: []const [*c]c.lo_arg) !bool { _ = types; _ = argv; var parts = std.mem.tokenizeScalar(u8, path, '/'); _ = parts.next(); const name = parts.next() orelse unreachable; if (self.cache.textures.fetchRemove(name)) |kv| { // @TODO: find all references to kv.value.texture in UniformCache and unassign kv.value.deinit(self.cache.allocator); self.cache.allocator.free(kv.key); } else return false; return false; } };