const std = @import("std"); const c = @import("c.zig"); 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: 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_one( comptime T: type, dest: *(if (T == bool) c.GLuint else 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)); }, bool => { dest.* = switch (types[0]) { 'T' => c.GL_TRUE, 'F' => c.GL_FALSE, else => return error.typeMismatch, }; }, else => return error.invalidType, } } fn set_array( comptime T: type, dest: [](if (T == bool) c.GLuint else 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, } } 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, 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, }; 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); } 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 { while (c.lo_server_recv_noblock(self.server, 0) > 0) {} } pub fn destroy(self: *ControlServer) void { c.lo_server_free(self.server); var tit = self.sources.iterator(); while (tit.next()) |entry| { entry.value_ptr.*.deinit(self.allocator); self.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(); 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 set_texture( self: *ControlServer, 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 = self.sources.get(name) orelse return error.texNotFound; if (source.texture.type != texture_type) return error.wrongTextureType; dest.* = &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 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(bool, 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(bool, val.*[0..], argv, types), .BOOL_VEC3 => |val| set_array(bool, val.*[0..], argv, types), .BOOL_VEC4 => |val| set_array(bool, 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| 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('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('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, 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; } };