75474dcc90
The vendored rate controller packs its wavelet block index into 16 bits (RDOperation.block_offset_saving), so a mode whose 32x32-block count exceeds u16::MAX wraps inside the controller and corrupts the bitstream — ~8K-class 4:4:4 territory. Compute the exact count (`block_count_32x32`, the counting walk of upstream init_block_meta, pinned against the validated Apple WaveletLayout) and expose `pyrowave_mode_fits_rdo`; the negotiator downgrades such a session to 4:2:0 before the Welcome (the honest-downgrade channel), and both encoders refuse outright if one slips through rather than emit a wrapped stream. Vendor patches: 0002-rdo-saving-clamp (analyze_rate_control.comp clamps the saving accumulation to the target, same overrun class as 0001; slangmosh.hpp regenerated), 0003-devel-encode-16bit-read (devel tool y4m 16-bit plane reads; tool-only, kept so the vendored source stays honest). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
265 lines
7.7 KiB
C++
265 lines
7.7 KiB
C++
// Copyright (c) 2025 Hans-Kristian Arntzen
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
#include <string.h>
|
|
|
|
#include "global_managers_init.hpp"
|
|
#include "device.hpp"
|
|
#include "context.hpp"
|
|
#include "pyrowave_encoder.hpp"
|
|
#include "yuv4mpeg.hpp"
|
|
#include "shaders/slangmosh.hpp"
|
|
|
|
using namespace Granite;
|
|
using namespace Vulkan;
|
|
|
|
struct EncodedBuffer
|
|
{
|
|
BufferHandle payload;
|
|
BufferHandle meta;
|
|
Fence fence;
|
|
};
|
|
|
|
static EncodedBuffer run_encoder_frame(CommandBufferHandle &cmd,
|
|
PyroWave::Encoder &enc,
|
|
const PyroWave::ViewBuffers &inputs,
|
|
uint32_t frame_index,
|
|
uint32_t bitstream_size)
|
|
{
|
|
auto &device = cmd->get_device();
|
|
|
|
EncodedBuffer encoded;
|
|
BufferCreateInfo buffer_info = {};
|
|
buffer_info.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
|
|
|
|
buffer_info.size = enc.get_meta_required_size();
|
|
buffer_info.domain = BufferDomain::Device;
|
|
auto meta = device.create_buffer(buffer_info);
|
|
buffer_info.domain = BufferDomain::CachedHost;
|
|
encoded.meta = device.create_buffer(buffer_info);
|
|
|
|
buffer_info.size = bitstream_size + 2 * enc.get_meta_required_size();
|
|
buffer_info.domain = BufferDomain::Device;
|
|
auto bitstream = device.create_buffer(buffer_info);
|
|
buffer_info.domain = BufferDomain::CachedHost;
|
|
encoded.payload = device.create_buffer(buffer_info);
|
|
|
|
PyroWave::Encoder::BitstreamBuffers buffers = {};
|
|
buffers.meta.buffer = meta.get();
|
|
buffers.meta.size = meta->get_create_info().size;
|
|
buffers.bitstream.buffer = bitstream.get();
|
|
buffers.bitstream.size = bitstream->get_create_info().size;
|
|
buffers.target_size = bitstream_size;
|
|
|
|
enc.encode(*cmd, inputs, buffers);
|
|
cmd->copy_buffer(*encoded.payload, *bitstream);
|
|
cmd->copy_buffer(*encoded.meta, *meta);
|
|
cmd->barrier(VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_TRANSFER_WRITE_BIT,
|
|
VK_PIPELINE_STAGE_HOST_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT,
|
|
VK_ACCESS_HOST_READ_BIT);
|
|
device.submit(cmd, &encoded.fence);
|
|
device.next_frame_context();
|
|
|
|
LOGI("Submitted frame %06u ...\n", frame_index);
|
|
return encoded;
|
|
}
|
|
|
|
struct YCbCrImages
|
|
{
|
|
Vulkan::ImageHandle images[3];
|
|
PyroWave::ViewBuffers views;
|
|
};
|
|
|
|
static YCbCrImages create_ycbcr_images(Device &device, int width, int height, VkFormat fmt, PyroWave::ChromaSubsampling chroma)
|
|
{
|
|
YCbCrImages images;
|
|
auto info = ImageCreateInfo::immutable_2d_image(width, height, fmt);
|
|
info.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT |
|
|
VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
|
info.initial_layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
|
|
images.images[0] = device.create_image(info);
|
|
device.set_name(*images.images[0], "Y");
|
|
|
|
if (chroma == PyroWave::ChromaSubsampling::Chroma420)
|
|
{
|
|
info.width >>= 1;
|
|
info.height >>= 1;
|
|
}
|
|
|
|
images.images[1] = device.create_image(info);
|
|
device.set_name(*images.images[1], "Cb");
|
|
|
|
images.images[2] = device.create_image(info);
|
|
device.set_name(*images.images[2], "Cr");
|
|
|
|
for (int i = 0; i < 3; i++)
|
|
images.views.planes[i] = &images.images[i]->get_view();
|
|
|
|
return images;
|
|
}
|
|
|
|
static bool write_payload(FILE *file, PyroWave::Encoder &encoder, Device &device, const Buffer &payload, const Buffer &meta)
|
|
{
|
|
auto *mapped_payload = device.map_host_buffer(payload, MEMORY_ACCESS_READ_BIT);
|
|
auto *mapped_meta = device.map_host_buffer(meta, MEMORY_ACCESS_READ_BIT);
|
|
|
|
std::vector<uint8_t> packetized_data(payload.get_create_info().size);
|
|
|
|
PyroWave::Encoder::Packet packet = {};
|
|
if (encoder.packetize(&packet, payload.get_create_info().size,
|
|
packetized_data.data(), packetized_data.size(),
|
|
mapped_meta, mapped_payload) != 1)
|
|
{
|
|
LOGE("Something went terribly wrong ...\n");
|
|
std::terminate();
|
|
}
|
|
|
|
uint32_t u32_size = packet.size;
|
|
if (fwrite(&u32_size, sizeof(u32_size), 1, file) != 1)
|
|
return false;
|
|
return fwrite(packetized_data.data() + packet.offset, 1, packet.size, file) == packet.size;
|
|
}
|
|
|
|
static void run_encoder(Device &device, const char *out_path, const char *in_path, uint32_t bitstream_size)
|
|
{
|
|
YUV4MPEGFile input;
|
|
|
|
if (!input.open_read(in_path))
|
|
{
|
|
LOGE("Failed to open input file.\n");
|
|
return;
|
|
}
|
|
|
|
struct FILEDeleter { void operator()(FILE *file) { if (file) fclose(file); } };
|
|
std::unique_ptr<FILE, FILEDeleter> out;
|
|
|
|
out.reset(fopen(out_path, "wb"));
|
|
if (!out)
|
|
{
|
|
LOGE("Failed to open output file.\n");
|
|
return;
|
|
}
|
|
|
|
if (fwrite("PYROWAVE", 1, 8, out.get()) != 8)
|
|
{
|
|
LOGE("Failed to write magic.\n");
|
|
return;
|
|
}
|
|
|
|
int32_t width = input.get_width();
|
|
int32_t height = input.get_height();
|
|
auto fmt = YUV4MPEGFile::format_to_bytes_per_component(input.get_format()) == 2 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R8_UNORM;
|
|
auto chroma = YUV4MPEGFile::format_has_subsampling(input.get_format()) ? PyroWave::ChromaSubsampling::Chroma420 : PyroWave::ChromaSubsampling::Chroma444;
|
|
|
|
int32_t u32_params[8] = {
|
|
width, height, int(input.get_format()), int(chroma), input.is_full_range(),
|
|
input.get_frame_rate_num(), input.get_frame_rate_den(), 0 /* placeholder for unknown chroma siting */
|
|
};
|
|
|
|
if (fwrite(u32_params, sizeof(u32_params), 1, out.get()) != 1)
|
|
{
|
|
LOGE("Failed to write u32 params.\n");
|
|
return;
|
|
}
|
|
|
|
auto inputs = create_ycbcr_images(device, width, height, fmt, chroma);
|
|
|
|
PyroWave::Encoder enc;
|
|
if (!enc.init(&device, width, height, chroma))
|
|
return;
|
|
|
|
EncodedBuffer queue[2];
|
|
uint32_t frame_index = 0;
|
|
|
|
for (;;)
|
|
{
|
|
auto &q = queue[frame_index & 1];
|
|
if (q.fence)
|
|
{
|
|
q.fence->wait();
|
|
q.fence.reset();
|
|
if (!write_payload(out.get(), enc, device, *q.payload, *q.meta))
|
|
{
|
|
LOGE("Failed to write payload.\n");
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!input.begin_frame())
|
|
break;
|
|
|
|
auto cmd = device.request_command_buffer();
|
|
|
|
for (auto &img : inputs.images)
|
|
{
|
|
cmd->image_barrier(*img, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
|
0, 0,
|
|
VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT);
|
|
}
|
|
|
|
for (auto &img : inputs.images)
|
|
{
|
|
auto *y = cmd->update_image(*img);
|
|
// PUNKTFUNK PATCH 0003: size the plane read in BYTES, not texels — 16-bit y4m
|
|
// inputs otherwise read half a plane and desync the stream after frame 1.
|
|
if (!input.read(y, size_t(img->get_width()) * img->get_height() *
|
|
YUV4MPEGFile::format_to_bytes_per_component(input.get_format())))
|
|
{
|
|
LOGE("Failed to read plane.\n");
|
|
device.submit_discard(cmd);
|
|
break;
|
|
}
|
|
}
|
|
|
|
for (auto &img : inputs.images)
|
|
{
|
|
cmd->image_barrier(*img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
|
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
|
|
VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT,
|
|
VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_SAMPLED_READ_BIT);
|
|
}
|
|
|
|
queue[frame_index & 1] = run_encoder_frame(cmd, enc, inputs.views, frame_index, bitstream_size);
|
|
frame_index++;
|
|
}
|
|
|
|
frame_index--;
|
|
|
|
auto &q = queue[frame_index & 1];
|
|
if (q.fence)
|
|
{
|
|
q.fence->wait();
|
|
q.fence.reset();
|
|
if (!write_payload(out.get(), enc, device, *q.payload, *q.meta))
|
|
LOGE("Failed to write payload.\n");
|
|
}
|
|
}
|
|
|
|
static void run_encoder(const char *out_path, const char *in_path, uint32_t bytes_per_frame)
|
|
{
|
|
if (!Context::init_loader(nullptr))
|
|
return;
|
|
|
|
Context ctx;
|
|
|
|
if (!ctx.init_instance_and_device(nullptr, 0, nullptr, 0, CONTEXT_CREATION_ENABLE_PUSH_DESCRIPTOR_BIT))
|
|
return;
|
|
|
|
Device dev;
|
|
dev.set_context(ctx);
|
|
|
|
run_encoder(dev, out_path, in_path, bytes_per_frame);
|
|
}
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
if (argc != 4)
|
|
{
|
|
LOGE("Usage: pyrowave-encode <input.y4m> <output.pyrowave> <bytes_per_frame>\n");
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
run_encoder(argv[2], argv[1], strtoul(argv[3], nullptr, 0));
|
|
}
|