// video player implemented for sgpu, // uses theora/ogg playback from file. allocator: std.mem.Allocator, videoFile: OggTheora_File = undefined, width: c_int = undefined, height: c_int = undefined, uvWidth: c_int = undefined, uvHeight: c_int = undefined, fps: f64 = undefined, fmt: c.th_pixel_fmt = undefined, playbackStarted: bool = false, currentFrame: c_int = 0, currentTime: f64 = 0.0, showDebug: bool = true, device: *gpu.GPUDevice = undefined, // TH_PF_420, // /**Currently reserved.*/ // TH_PF_RSVD, // /**Chroma decimation by 2 in the X direction (4:2:2). // The Cb and Cr chroma planes are half the width of the luma plane, but full // height.*/ // TH_PF_422, // /**No chroma decimation (4:4:4). // The Cb and Cr chroma planes are full width and full height.*/ // TH_PF_444, // /**The total number of currently defined pixel formats.*/ // TH_PF_NFORMATS yTexture: *gpu.GPUTexture = undefined, uTexture: *gpu.GPUTexture = undefined, vTexture: *gpu.GPUTexture = undefined, ySampler: *gpu.GPUSampler = undefined, uSampler: *gpu.GPUSampler = undefined, vSampler: *gpu.GPUSampler = undefined, gpuTransferBuffer: *gpu.GPUTransferBuffer = undefined, fn log(comptime fmt: []const u8, args: anytype) void { core.engine_log("[Videoplayer] " ++ fmt, args); } pub fn create(allocator: std.mem.Allocator) !*@This() { const self = try allocator.create(@This()); self.* = .{ .allocator = allocator, }; log("initialized", .{}); self.device = rend.context().device; return self; } pub fn startPlayback(self: *@This(), filePath: [*c]const u8) !void { if (c.tf_fopen(filePath, &self.videoFile) < 0) { return error.UnableToOpenTestFile; } c.tf_videoinfo(&self.videoFile, &self.width, &self.height, &self.fps, &self.fmt); log("playing video {s}", .{filePath}); log("dimensions : {d}x{d} px {d}fps format={d}", .{ self.width, self.height, self.fps, self.fmt }); if (self.fmt == c.TH_PF_420) { log("PF = 420", .{}); // /* Subsampled in both dimensions */ self.uvWidth = @divTrunc(self.width, 2); self.uvHeight = @divTrunc(self.height, 2); } else if (self.fmt == c.TH_PF_422) { log("PF = 422", .{}); // /* Subsampled only horizontally */ self.uvWidth = @divTrunc(self.width, 2); self.uvHeight = self.height; } else { // /* No subsampling at all... */ log("PF = ANY", .{}); self.uvWidth = self.width; self.uvHeight = self.height; } try self.createTextures(); // self.frame = try self.allocator.alloc(u8, @intCast(self.width * self.height * 2)); self.playbackStarted = true; const frame = self.acquireVideoFrameBuffer(); while (c.tf_readvideo(&self.videoFile, frame.ptr, 1) == 0) {} const cmd = rend.context().device.acquireGPUCommandBuffer(); self.device.unmapGPUTransferBuffer(self.gpuTransferBuffer); self.releaseAndUploadFrameBuffer(cmd); _ = cmd.submitGPUCommandBuffer(); } fn getFrameLength(self: @This()) c_int { return self.width * self.height; } fn acquireVideoFrameBuffer(self: *@This()) []u8 { var slice: []u8 = undefined; slice.ptr = rend.context().device.mapGPUTransferBuffer(self.gpuTransferBuffer, false); slice.len = @intCast(self.width * self.width * 2); return slice; } fn releaseAndUploadFrameBuffer(self: *@This(), cmd: *gpu.GPUCommandBuffer) void { const copyPass = cmd.beginGPUCopyPass(); // copy Y buffer copyPass.uploadToGPUTexture( &.{ .transfer_buffer = self.gpuTransferBuffer, .offset = 0, .pixels_per_row = @intCast(self.width), .rows_per_layer = @intCast(self.height), }, &std.mem.zeroInit(gpu.GPUTextureRegion, .{ .texture = self.yTexture, .w = @as(u32, @intCast(self.width)), .h = @as(u32, @intCast(self.height)), .d = 1, }), true, ); // copy u buffer copyPass.uploadToGPUTexture( &.{ .transfer_buffer = self.gpuTransferBuffer, .offset = @intCast(self.getFrameLength()), .pixels_per_row = @intCast(self.uvWidth), .rows_per_layer = @intCast(self.uvHeight), }, &std.mem.zeroInit(gpu.GPUTextureRegion, .{ .texture = self.uTexture, .w = @as(u32, @intCast(self.uvWidth)), .h = @as(u32, @intCast(self.uvHeight)), .d = 1, }), true, ); // copy v buffer copyPass.uploadToGPUTexture( &.{ .transfer_buffer = self.gpuTransferBuffer, .offset = @intCast(self.getFrameLength() + self.uvWidth * self.uvHeight), .pixels_per_row = @intCast(self.uvWidth), .rows_per_layer = @intCast(self.uvHeight), }, &std.mem.zeroInit(gpu.GPUTextureRegion, .{ .texture = self.vTexture, .w = @as(u32, @intCast(self.uvWidth)), .h = @as(u32, @intCast(self.uvHeight)), .d = 1, }), true, ); copyPass.endGPUCopyPass(); } fn createTextureInner(width: c_int, height: c_int) !*gpu.GPUTexture { const ctx = rend.context(); const gpuTexture = ctx.device.createGPUTexture(&std.mem.zeroInit(gpu.GPUTextureCreateInfo, .{ .type = .texturetype2d, .format = .textureformatR8Unorm, .usage = .{ .textureusageSampler = true }, .width = @as(u32, @intCast(width)), .height = @as(u32, @intCast(height)), .layer_count_or_depth = 1, .num_levels = 1, })); return gpuTexture; } pub fn createTextures(self: *@This()) !void { self.yTexture = try createTextureInner(self.width, self.height); self.uTexture = try createTextureInner(self.uvWidth, self.uvHeight); self.vTexture = try createTextureInner(self.uvWidth, self.uvHeight); // create transfer Buffer self.gpuTransferBuffer = self.device.createGPUTransferBuffer(&.{ .usage = .transferbufferusageUpload, .size = @as(u32, @intCast(self.width * self.height * 2)), .props = 0, }); self.ySampler = createVideoSampler(); self.uSampler = createVideoSampler(); self.vSampler = createVideoSampler(); } pub fn tick(self: *@This(), dt: f64) void { if (!self.playbackStarted) return; if (c.tf_eos(&self.videoFile) != 0) { c.tf_reset(&self.videoFile); } self.currentTime += dt; const thisFrame: c_int = @intFromFloat(self.currentTime * self.fps); // read the file's new frame if (thisFrame > self.currentFrame) { const frame = self.acquireVideoFrameBuffer(); const newFrame = c.tf_readvideo(&self.videoFile, frame.ptr, thisFrame - self.currentFrame); if (newFrame != 0) { const cmd = rend.context().device.acquireGPUCommandBuffer(); self.releaseAndUploadFrameBuffer(cmd); _ = cmd.submitGPUCommandBuffer(); // kick off texture upload here } self.device.unmapGPUTransferBuffer(self.gpuTransferBuffer); self.currentFrame = thisFrame; } } fn bindVideoTextures(p: ?*anyopaque) void { const self: *@This() = @ptrCast(@alignCast(p)); const ctx = rend.context(); const renderpass: *gpu.GPURenderPass = ctx.state.pass.?; renderpass.bindGPUFragmentSamplers(0, &.{ .sampler = self.ySampler, .texture = self.yTexture }, 1); renderpass.bindGPUFragmentSamplers(1, &.{ .sampler = self.uSampler, .texture = self.uTexture }, 1); renderpass.bindGPUFragmentSamplers(2, &.{ .sampler = self.vSampler, .texture = self.vTexture }, 1); } fn createVideoSampler() *gpu.GPUSampler { return rend.context().device.createGPUSampler(&std.mem.zeroInit(gpu.GPUSamplerCreateInfo, .{ .min_filter = .filterLinear, .mag_filter = .filterLinear, .mipmap_mode = .samplermipmapmodeNearest, .address_mode_u = .sampleraddressmodeRepeat, .address_mode_v = .sampleraddressmodeRepeat, .address_mode_w = .sampleraddressmodeRepeat, })); } // creates a video player entity pub fn createVideoPlayerEntity(self: *@This()) !core.Entity { const entity = try core.createEntity(); const scene = entity.addComponent(core.Scene).?; const size = 5.0; const h: f32 = @floatFromInt(self.height); const w: f32 = @floatFromInt(self.width); scene.setScale(size, size, size * (h / w)); scene.setRotation(core.Rotation.eulerX(core.radians(90.0))); if (entity.addComponent(rend.MeshComponent)) |mesh| { mesh.cmrf = bindVideoTextures; mesh.cmrf_ctx = self; mesh.textureMode.samplerMode = .videoYUV; mesh.setMesh("m_plane"); } return entity; } pub fn destroy(self: *@This()) void { core.engine_logs("[Videoplayer] destroying player"); if (self.playbackStarted) {} self.allocator.destroy(self); } const std = @import("std"); const backlog = @import("Backlog"); const core = backlog.core; const rend = backlog.rend; const c = @import("theorafile").c; const gpu = rend.renderer.gpu; const OggTheora_File = c.OggTheora_File; const ig = backlog.imgui.api;