const std = @import("std"); const c = @import("c.zig").c; const gl = @import("gl.zig"); const src = @import("source.zig"); const cfg = @import("config.zig"); const util = @import("util.zig"); const build_config = @import("build_config"); fn verify_args(expected: []const u8, got: []const u8) !void { if (!std.mem.eql(u8, expected, got)) { std.debug.print("expected '{s}' but got {s}\n", .{ expected, got }); return error.typeMismatch; } } fn verify_args_all(expected: u8, got: []const u8) !void { for (got) |typ| { if (typ != expected) { std.debug.print("expected '{c}' but got '{c}' element (args are {s})\n", .{ expected, typ, got }); return error.typeMismatch; } } } 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_all('f', types); for (dest, 0..) |*v, i| v.* = argv[i].*.f; }, f64 => { try verify_args_all('d', types); for (dest, 0..) |*v, i| v.* = argv[i].*.d; }, i32 => { try verify_args_all('i', types); for (dest, 0..) |*v, i| v.* = argv[i].*.i; }, u32 => { try verify_args_all('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, } } pub const ControlServer = struct { server: c.lo_server, sources: std.StringHashMap(*src.Source), cache: *gl.UniformCache, config: *cfg.Config, constants: *const gl.Constants, progress: std.Progress.Node, allocator: std.mem.Allocator, dirty: bool, pub fn init( allocator: std.mem.Allocator, progress: std.Progress.Node, cache: *gl.UniformCache, config: *cfg.Config, constants: *const gl.Constants, ) !*ControlServer { var self: *ControlServer = try allocator.create(ControlServer); self.* = .{ .server = null, .sources = std.StringHashMap(*src.Source).init(cache.allocator), .cache = cache, .config = config, .constants = constants, .progress = progress, .allocator = allocator, .dirty = false, }; 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; try self.add_method(&.{"/shader"}, "s", handle_shader); // try self.add_method(&.{ "/reload" }, "", handle_reload); try self.add_method(&.{ "/uniform", "*" }, null, handle_uniform); if (build_config.have_ffmpeg) { try self.add_method(&.{ "/source", "*", "video" }, null, handle_texture_video); try self.add_method(&.{ "/source", "*", "stream" }, null, handle_texture_stream); try self.add_method(&.{ "/source", "*", "buffered-stream" }, null, handle_texture_buffered_stream); } if (build_config.have_tsv) { try self.add_method(&.{ "/source", "*", "tsv" }, "ss", handle_texture_tsv); } try self.add_method(&.{ "/source", "*", "destroy" }, "", handle_destroy_texture); return self; } fn add_method(self: *ControlServer, pieces: []const []const u8, types: ?[]const u8, handler: anytype) !void { try self.add_method_for(ControlServer, self, pieces, types, handler); } pub fn add_method_for(self: *ControlServer, comptime Self: type, data: *Self, pieces: []const []const u8, types: ?[]const u8, handler: anytype) !void { const path = try util.tmpJoinZ("/", pieces); const wrapped = 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 i_self: *Self = @ptrCast(@alignCast(userdata.?)); const i_path = std.mem.span(_path.?); const i_types = std.mem.span(_types.?); const args = if (_argv) |argv| argv[0..@intCast(argc)] else &.{}; _ = msg; const res = handler(i_self, i_path, i_types, args) catch |err| { std.debug.print("Error handling {s}: {}\n", .{ i_path, err }); return 1; }; return if (res) 0 else 1; } }.wrapped; _ = c.lo_server_add_method(self.server, path, if (types) |t| t.ptr else null, wrapped, @as(*anyopaque, @ptrCast(data))); } pub fn del_method(self: *ControlServer, pieces: []const []const u8, types: ?[]const u8) !void { const path = try util.tmpJoinZ("/", pieces); _ = c.lo_server_del_method(self.server, path, if (types) |t| t.ptr else null); } pub fn update(self: *ControlServer) void { var names: [256][]const u8 = undefined; var i: usize = 0; var tit = self.sources.iterator(); while (tit.next()) |entry| { const remove = entry.value_ptr.*.update(); if (remove) { std.debug.print("delete source {s}\n", .{entry.key_ptr.*}); entry.value_ptr.*.deinit(self.cache.allocator); names[i] = entry.key_ptr.*; i += 1; } } for (names[0..i]) |name| { _ = self.sources.remove(name); self.cache.allocator.free(name); } while (c.lo_server_recv_noblock(self.server, 0) > 0) {} } pub fn deinit(self: *ControlServer) void { c.lo_server_free(self.server); var tit = self.sources.iterator(); while (tit.next()) |entry| { entry.value_ptr.*.deinit(self.cache.allocator); self.cache.allocator.free(entry.key_ptr.*); } self.sources.deinit(); } 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(); self.dirty = true; 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 set_texture( self: *ControlServer, dest: []?*gl.Texture, texture_type: gl.Texture.Type, types: []const u8, argv: []const [*c]c.lo_arg, ) !void { try verify_args_all('s', types); if (types.len != dest.len) return error.sizeMismatch; for (argv, dest) |arg, *tex| { const nameZ: [*:0]const u8 = @ptrCast(&arg.*.s); const name = std.mem.span(nameZ); const source = self.sources.get(name) orelse return error.texNotFound; if (source.texture.type != texture_type) return error.wrongTextureType; tex.* = &source.texture; } } 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; 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; } } value = value.flatten(); if (std.mem.eql(u8, types, "s")) { const param_stringZ: [*:0]const u8 = @ptrCast(&argv[0].*.s); const param_string = std.mem.span(param_stringZ); if (std.mem.startsWith(u8, param_string, "/source/")) { var param_parts = std.mem.tokenizeScalar(u8, param_string, '/'); _ = param_parts.next(); // skip /source const source_name = param_parts.next().?; const param_name = param_parts.next().?; const source = self.sources.get(source_name) orelse return error.texNotFound; try source.updateUniform(param_name, value); uniform.setShaderValue(self.cache.shader.*); return true; } } try switch (value) { inline .FLOAT, .DOUBLE, .INT, .UNSIGNED_INT, .BOOL, => |val| set_array(@TypeOf(val[0]), val, argv, types), .SAMPLER_2D, .SAMPLER_2D_SHADOW, .INT_SAMPLER_2D, .UNSIGNED_INT_SAMPLER_2D, => |val| self.set_texture(val, .TEXTURE_2D, types, argv), .SAMPLER_2D_ARRAY, .SAMPLER_2D_ARRAY_SHADOW, .INT_SAMPLER_2D_ARRAY, .UNSIGNED_INT_SAMPLER_2D_ARRAY, => |val| self.set_texture(val, .TEXTURE_2D_ARRAY, types, argv), .SAMPLER_3D, .INT_SAMPLER_3D, .UNSIGNED_INT_SAMPLER_3D, => |val| self.set_texture(val, .TEXTURE_3D, types, argv), else => error.uniformNotSupported, }; uniform.setShaderValue(self.cache.shader.*); return true; } fn add_source(self: *ControlServer, name: []const u8, source: *src.Source) !void { const key = try self.cache.allocator.dupe(u8, name); errdefer self.cache.allocator.free(key); try self.sources.put(key, source); source.register(name, self); } fn handle_texture_video(self: *ControlServer, path: []const u8, types: []const u8, argv: []const [*c]c.lo_arg) !bool { try verify_args_all('s', types); var parts = std.mem.tokenizeScalar(u8, path, '/'); _ = parts.next(); const name = parts.next() orelse unreachable; if (self.sources.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 formatZ: ?[*:0]const u8 = if (argv.len > 2) @ptrCast(&argv[2].*.s) else null; const args = if (argv.len > 3) argv[3..] else &.{}; const ffmpeg = @import("ffmpeg.zig"); const source = try ffmpeg.VideoSource.init(self.cache.allocator, self.progress, texture_type, filenameZ, formatZ, args); try self.add_source(name, source); return true; } fn handle_texture_stream(self: *ControlServer, path: []const u8, types: []const u8, argv: []const [*c]c.lo_arg) !bool { try verify_args_all('s', types); var parts = std.mem.tokenizeScalar(u8, path, '/'); _ = parts.next(); const name = parts.next() orelse unreachable; if (self.sources.contains(name)) return error.textureNameTaken; const texture_typeZ: [*:0]const u8 = @ptrCast(&argv[0].*.s); const filenameZ: [*:0]const u8 = @ptrCast(&argv[1].*.s); const formatZ: ?[*:0]const u8 = if (argv.len > 2) @ptrCast(&argv[2].*.s) else null; const args = if (argv.len > 3) argv[3..] else &.{}; if (!std.mem.eql(u8, std.mem.span(texture_typeZ), "TEXTURE_2D")) return error.textureTypeInvalid; const ffmpeg = @import("ffmpeg.zig"); const source = try ffmpeg.StreamSource.init(self.cache.allocator, self.constants, .TEXTURE_2D, 1, filenameZ, formatZ, args); try self.add_source(name, source); return true; } fn handle_texture_buffered_stream(self: *ControlServer, path: []const u8, types: []const u8, argv: []const [*c]c.lo_arg) !bool { try verify_args("sis", types[0..3]); try verify_args_all('s', types[3..]); var parts = std.mem.tokenizeScalar(u8, path, '/'); _ = parts.next(); const name = parts.next() orelse unreachable; if (self.sources.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 depth: i32 = argv[1].*.i; const filenameZ: [*:0]const u8 = @ptrCast(&argv[2].*.s); const formatZ: ?[*:0]const u8 = if (argv.len > 3) @ptrCast(&argv[3].*.s) else null; const args = if (argv.len > 4) argv[4..] else &.{}; const ffmpeg = @import("ffmpeg.zig"); const source = try ffmpeg.StreamSource.init(self.cache.allocator, self.constants, texture_type, depth, filenameZ, formatZ, args); try self.add_source(name, 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.sources.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, self.constants, texture_type, tsv_nameZ); try self.add_source(name, 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.sources.fetchRemove(name)) |kv| { // @TODO: find all references to kv.value.texture in UniformCache and unassign kv.value.unregister(name, self); kv.value.deinit(self.cache.allocator); self.cache.allocator.free(kv.key); } else return false; return true; } };