288 lines
8.9 KiB
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;
|