Backlog/extras/videoplayer/src/videoplayer.zig

288 lines
8.9 KiB
Zig

// 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;