feat(host): vendor PyroWave + minimal Granite subset as crates/pyrowave-sys

Phase 0 of design/pyrowave-codec-plan.md — the opt-in wired-LAN ultra-low-
latency codec. Vendored at upstream 509e4f88 (API 0.4.0, Granite 44362775,
volk + vulkan-headers pins in PUNKTFUNK-VENDOR.txt), pruned to the 6.6 MB
the standalone no-renderer build needs; scripts/vendor-pyrowave.sh
reproduces the tree (a pin bump is protocol-affecting, plan §4.2).

build.rs drives the wrapper CMakeLists (static archives incl. a static
C-API lib upstream only ships shared) + bindgen over pyrowave.h; Linux and
Windows only, empty stub elsewhere (Apple gets a native Metal port, §4.7).
Offline-safe by construction: no network, no system lib, vendored Vulkan
headers — same model as the opus dep (flatpak builder has no network).

Phase-0 validation on .21 (RTX 5070 Ti, driver 610.43.03):
- upstream pyrowave-c-test + interop test (incl. dmabuf/DRM-modifier
  Vulkan<->Vulkan) pass, from the pristine AND the pruned tree
- GPU kernel times at ~1.6 bpp noise: encode/decode 0.090/0.042 ms @800p,
  0.146/0.067 @1080p, 0.226/0.103 @1440p, 0.477/0.201 @4K — order of
  magnitude under NVENC's 1-2 ms retrieve, CBR lands within ~100 B of
  target
- cargo test -p pyrowave-sys green (static link + API-version pin check)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 00:35:10 +02:00
parent 1b73361372
commit 4c3b11445c
396 changed files with 140058 additions and 0 deletions
@@ -0,0 +1,381 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "compiler.hpp"
#include "shaderc/shaderc.hpp"
#include "path_utils.hpp"
#include "logging.hpp"
#include "string_helpers.hpp"
#include "spirv-tools/libspirv.hpp"
namespace Granite
{
GLSLCompiler::GLSLCompiler(FilesystemInterface &iface_)
: iface(iface_)
{
}
static Stage stage_from_path(const std::string &path)
{
auto ext = Path::ext(path);
if (ext == "vert")
return Stage::Vertex;
else if (ext == "frag")
return Stage::Fragment;
else if (ext == "comp")
return Stage::Compute;
else if (ext == "task")
return Stage::Task;
else if (ext == "mesh")
return Stage::Mesh;
else
return Stage::Unknown;
}
static Stage convert_stage(const std::string &stage)
{
if (stage == "vertex")
return Stage::Vertex;
else if (stage == "compute")
return Stage::Compute;
else if (stage == "fragment")
return Stage::Fragment;
else if (stage == "task")
return Stage::Task;
else if (stage == "mesh")
return Stage::Mesh;
else
return Stage::Unknown;
}
bool GLSLCompiler::set_source_from_file(const std::string &path, Stage forced_stage)
{
if (!iface.load_text_file(path, source))
{
LOGE("Failed to load shader: %s\n", path.c_str());
return false;
}
source_path = path;
if (forced_stage != Stage::Unknown)
stage = forced_stage;
else
stage = stage_from_path(path);
return stage != Stage::Unknown;
}
bool GLSLCompiler::set_source_from_file_multistage(const std::string &path)
{
if (iface.load_text_file(path, source))
{
LOGE("Failed to load shader: %s\n", path.c_str());
return false;
}
source_path = path;
stage = Stage::Unknown;
return true;
}
void GLSLCompiler::set_include_directories(const std::vector<std::string> *include_directories_)
{
include_directories = include_directories_;
}
bool GLSLCompiler::find_include_path(const std::string &source_path_, const std::string &include_path,
std::string &included_path, std::string &included_source)
{
auto relpath = Path::relpath(source_path_, include_path);
if (iface.load_text_file(relpath, included_source))
{
included_path = relpath;
return true;
}
if (include_directories)
{
for (auto &include_dir : *include_directories)
{
auto path = Path::join(include_dir, include_path);
if (iface.load_text_file(path, included_source))
{
included_path = path;
return true;
}
}
}
return false;
}
bool GLSLCompiler::parse_variants(const std::string &source_, const std::string &path)
{
auto lines = Util::split(source_, "\n");
unsigned line_index = 1;
size_t offset;
for (auto &line : lines)
{
// This check, followed by the include statement check below isn't even remotely correct,
// but we only have to care about shaders we control here.
if ((offset = line.find("//")) != std::string::npos)
line = line.substr(0, offset);
if ((offset = line.find("#include \"")) != std::string::npos)
{
auto include_path = line.substr(offset + 10);
if (!include_path.empty() && include_path.back() == '"')
include_path.pop_back();
std::string included_source;
if (!find_include_path(path, include_path, include_path, included_source))
{
LOGE("Failed to include GLSL file: %s\n", include_path.c_str());
return false;
}
preprocessed_source += Util::join("#line ", 1, " \"", include_path, "\"\n");
if (!parse_variants(included_source, include_path))
return false;
preprocessed_source += Util::join("#line ", line_index + 1, " \"", path, "\"\n");
dependencies.insert(include_path);
}
else if (line.find("#pragma optimize off") == 0)
{
optimization = Optimization::ForceOff;
preprocessed_source += "// #pragma optimize off";
preprocessed_source += '\n';
}
else if (line.find("#pragma optimize on") == 0)
{
optimization = Optimization::ForceOn;
preprocessed_source += "// #pragma optimize on";
preprocessed_source += '\n';
}
else if (line.find("#pragma stage ") == 0)
{
if (!preprocessed_source.empty())
{
preprocessed_sections.push_back({ preprocessing_active_stage, std::move(preprocessed_source) });
preprocessed_source = {};
}
preprocessing_active_stage = convert_stage(line.substr(14));
preprocessed_source += Util::join("#line ", line_index + 1, " \"", path, "\"\n");
}
else if (line.find("#pragma ") == 0)
{
pragmas.push_back(line.substr(8));
preprocessed_source += "// ";
preprocessed_source += line;
preprocessed_source += '\n';
}
else
{
preprocessed_source += line;
preprocessed_source += '\n';
auto first_non_space = line.find_first_not_of(' ');
if (first_non_space != std::string::npos && line[first_non_space] == '#')
{
auto keywords = Util::split(line.substr(first_non_space + 1), " ");
if (keywords.size() == 1)
{
auto &word = keywords.front();
if (word == "endif")
preprocessed_source += Util::join("#line ", line_index + 1, " \"", path, "\"\n");
}
}
}
line_index++;
}
return true;
}
bool GLSLCompiler::preprocess()
{
// Use a custom preprocessor where we only resolve includes once.
// The builtin glslang preprocessor is not suitable for this task,
// since we need to defer resolving defines.
preprocessed_source.clear();
pragmas.clear();
preprocessed_sections.clear();
preprocessing_active_stage = Stage::Unknown;
bool ret = parse_variants(source, source_path);
if (ret && !preprocessed_source.empty())
{
preprocessed_sections.push_back({ preprocessing_active_stage, std::move(preprocessed_source) });
preprocessed_source = {};
}
return ret;
}
Util::Hash GLSLCompiler::get_source_hash() const
{
Util::Hasher h;
for (auto &section : preprocessed_sections)
{
h.u32(uint32_t(section.stage));
h.string(section.source);
}
h.string(preprocessed_source);
return h.get();
}
std::vector<uint32_t> GLSLCompiler::compile(std::string &error_message, const std::vector<std::pair<std::string, int>> *defines) const
{
shaderc::Compiler compiler;
shaderc::CompileOptions options;
if (preprocessed_sections.empty())
{
error_message = "Need to preprocess source first.";
return {};
}
if (defines)
for (auto &define : *defines)
options.AddMacroDefinition(define.first, std::to_string(define.second));
#if GRANITE_COMPILER_OPTIMIZE
if (optimization != Optimization::ForceOff)
options.SetOptimizationLevel(shaderc_optimization_level_performance);
else
options.SetOptimizationLevel(shaderc_optimization_level_zero);
#else
options.SetOptimizationLevel(shaderc_optimization_level_zero);
#endif
if (!strip && (GRANITE_COMPILER_DEBUG || optimization == Optimization::ForceOff))
options.SetGenerateDebugInfo();
shaderc_shader_kind kind;
switch (stage)
{
case Stage::Vertex:
kind = shaderc_glsl_vertex_shader;
options.AddMacroDefinition("STAGE_VERTEX", "1");
break;
case Stage::Fragment:
kind = shaderc_glsl_fragment_shader;
options.AddMacroDefinition("STAGE_FRAGMENT", "1");
break;
case Stage::Compute:
kind = shaderc_glsl_compute_shader;
options.AddMacroDefinition("STAGE_COMPUTE", "1");
break;
case Stage::Task:
kind = shaderc_glsl_task_shader;
options.AddMacroDefinition("STAGE_TASK", "1");
break;
case Stage::Mesh:
kind = shaderc_glsl_mesh_shader;
options.AddMacroDefinition("STAGE_MESH", "1");
break;
default:
error_message = "Invalid shader stage.";
return {};
}
auto env = shaderc_env_version_vulkan_1_1;
auto spv_version = shaderc_spirv_version_1_3;
if (target == Target::Vulkan12)
{
env = shaderc_env_version_vulkan_1_2;
spv_version = shaderc_spirv_version_1_5;
}
else if (target == Target::Vulkan13)
{
env = shaderc_env_version_vulkan_1_3;
spv_version = shaderc_spirv_version_1_6;
}
options.SetTargetEnvironment(shaderc_target_env_vulkan, env);
options.SetTargetSpirv(spv_version);
options.SetSourceLanguage(shaderc_source_language_glsl);
shaderc::SpvCompilationResult result;
if (preprocessed_sections.size() == 1)
{
if (preprocessed_sections.front().stage != Stage::Unknown && preprocessed_sections.front().stage != stage)
{
error_message = "No preprocessed sections available.";
return {};
}
result = compiler.CompileGlslToSpv(preprocessed_sections.front().source, kind, source_path.c_str(), options);
}
else
{
std::string combined_source;
for (auto &section : preprocessed_sections)
if (section.stage == Stage::Unknown || section.stage == stage)
combined_source += section.source;
if (combined_source.empty())
{
error_message = "No preprocessed sections available.";
return {};
}
result = compiler.CompileGlslToSpv(combined_source, kind, source_path.c_str(), options);
}
error_message.clear();
if (result.GetCompilationStatus() != shaderc_compilation_status_success)
{
error_message = result.GetErrorMessage();
return {};
}
std::vector<uint32_t> compiled_spirv(result.cbegin(), result.cend());
#if 0
spvtools::SpirvTools core(target == Target::Vulkan13 ? SPV_ENV_VULKAN_1_3 : SPV_ENV_VULKAN_1_1);
core.SetMessageConsumer([&error_message](spv_message_level_t, const char *, const spv_position_t&, const char *message) {
error_message = message;
});
spvtools::ValidatorOptions opts;
opts.SetScalarBlockLayout(true);
if (!core.Validate(compiled_spirv.data(), compiled_spirv.size(), opts))
{
error_message += "\nFailed to validate SPIR-V.\n";
return {};
}
#endif
return compiled_spirv;
}
}