4c3b11445c
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>
384 lines
11 KiB
C++
384 lines
11 KiB
C++
/* 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 "asset_manager.hpp"
|
|
#include "thread_group.hpp"
|
|
#include <utility>
|
|
#include <algorithm>
|
|
|
|
namespace Granite
|
|
{
|
|
AssetManager::AssetManager()
|
|
{
|
|
asset_bank.reserve(AssetID::MaxIDs);
|
|
sorted_assets.reserve(AssetID::MaxIDs);
|
|
signal = std::make_unique<TaskSignal>();
|
|
for (uint64_t i = 0; i < timestamp; i++)
|
|
signal->signal_increment();
|
|
}
|
|
|
|
AssetManager::~AssetManager()
|
|
{
|
|
set_asset_instantiator_interface(nullptr);
|
|
signal->wait_until_at_least(timestamp);
|
|
for (uint32_t i = 0; i < id_count; i++)
|
|
pool.free(asset_bank[i]);
|
|
}
|
|
|
|
AssetID AssetManager::register_asset_nolock(FileHandle file, AssetClass asset_class, int prio)
|
|
{
|
|
auto *info = pool.allocate();
|
|
info->handle = std::move(file);
|
|
info->id.id = id_count;
|
|
info->prio = prio;
|
|
info->asset_class = asset_class;
|
|
AssetID ret = info->id;
|
|
asset_bank[id_count++] = info;
|
|
if (iface)
|
|
{
|
|
iface->set_id_bounds(id_count);
|
|
iface->set_asset_class(info->id, asset_class);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
void AssetInstantiatorInterface::set_asset_class(AssetID, AssetClass)
|
|
{
|
|
}
|
|
|
|
AssetID AssetManager::register_asset(FileHandle file, AssetClass asset_class, int prio)
|
|
{
|
|
std::lock_guard<std::mutex> holder{asset_bank_lock};
|
|
return register_asset_nolock(std::move(file), asset_class, prio);
|
|
}
|
|
|
|
AssetID AssetManager::register_asset(Filesystem &fs, const std::string &path, AssetClass asset_class, int prio)
|
|
{
|
|
std::lock_guard<std::mutex> holder{asset_bank_lock};
|
|
|
|
Util::Hasher h;
|
|
h.string(path);
|
|
if (auto *asset = file_to_assets.find(h.get()))
|
|
return asset->id;
|
|
|
|
auto file = fs.open(path);
|
|
if (!file)
|
|
return {};
|
|
|
|
auto id = register_asset_nolock(std::move(file), asset_class, prio);
|
|
asset_bank[id.id]->set_hash(h.get());
|
|
file_to_assets.insert_replace(asset_bank[id.id]);
|
|
return id;
|
|
}
|
|
|
|
void AssetManager::update_cost(AssetID id, uint64_t cost)
|
|
{
|
|
std::lock_guard<std::mutex> holder{cost_update_lock};
|
|
thread_cost_updates.push_back({ id, cost });
|
|
}
|
|
|
|
void AssetManager::set_asset_instantiator_interface(AssetInstantiatorInterface *iface_)
|
|
{
|
|
if (iface)
|
|
{
|
|
signal->wait_until_at_least(timestamp);
|
|
for (uint32_t id = 0; id < id_count; id++)
|
|
iface->release_asset(AssetID{id});
|
|
}
|
|
|
|
for (uint32_t i = 0; i < id_count; i++)
|
|
{
|
|
auto *a = asset_bank[i];
|
|
a->consumed = 0;
|
|
a->pending_consumed = 0;
|
|
a->last_used = 0;
|
|
}
|
|
total_consumed = 0;
|
|
|
|
iface = iface_;
|
|
if (iface)
|
|
{
|
|
iface->set_id_bounds(id_count);
|
|
for (uint32_t i = 0; i < id_count; i++)
|
|
iface->set_asset_class(AssetID{i}, asset_bank[i]->asset_class);
|
|
}
|
|
}
|
|
|
|
void AssetManager::mark_used_asset(AssetID id)
|
|
{
|
|
lru_append.push(id);
|
|
}
|
|
|
|
bool AssetManager::get_wants_mesh_assets() const
|
|
{
|
|
return wants_mesh_assets;
|
|
}
|
|
|
|
void AssetManager::enable_mesh_assets()
|
|
{
|
|
wants_mesh_assets = true;
|
|
}
|
|
|
|
void AssetManager::set_asset_budget(uint64_t cost)
|
|
{
|
|
transfer_budget = cost;
|
|
}
|
|
|
|
void AssetManager::set_asset_budget_per_iteration(uint64_t cost)
|
|
{
|
|
transfer_budget_per_iteration = cost;
|
|
}
|
|
|
|
bool AssetManager::set_asset_residency_priority(AssetID id, int prio)
|
|
{
|
|
std::lock_guard<std::mutex> holder{asset_bank_lock};
|
|
if (id.id >= id_count)
|
|
return false;
|
|
asset_bank[id.id]->prio = prio;
|
|
return true;
|
|
}
|
|
|
|
void AssetManager::adjust_update(const CostUpdate &update)
|
|
{
|
|
if (update.id.id < id_count)
|
|
{
|
|
auto *a = asset_bank[update.id.id];
|
|
total_consumed += update.cost - (a->consumed + a->pending_consumed);
|
|
a->consumed = update.cost;
|
|
a->pending_consumed = 0;
|
|
|
|
// A recently paged in image shouldn't be paged out right away in a situation where we're thrashing,
|
|
// that'd be very dumb.
|
|
a->last_used = timestamp;
|
|
}
|
|
}
|
|
|
|
uint64_t AssetManager::get_current_total_consumed() const
|
|
{
|
|
return total_consumed;
|
|
}
|
|
|
|
void AssetManager::update_costs_locked_assets()
|
|
{
|
|
{
|
|
std::lock_guard<std::mutex> holder_cost{cost_update_lock};
|
|
std::swap(cost_updates, thread_cost_updates);
|
|
}
|
|
|
|
for (auto &update : cost_updates)
|
|
adjust_update(update);
|
|
cost_updates.clear();
|
|
}
|
|
|
|
void AssetManager::update_lru_locked_assets()
|
|
{
|
|
lru_append.for_each_ranged([this](const AssetID *id, size_t count) {
|
|
for (size_t i = 0; i < count; i++)
|
|
if (id[i].id < id_count)
|
|
asset_bank[id[i].id]->last_used = timestamp;
|
|
});
|
|
lru_append.clear();
|
|
}
|
|
|
|
bool AssetManager::iterate_blocking(ThreadGroup &group, AssetID id)
|
|
{
|
|
if (!iface)
|
|
return false;
|
|
|
|
std::lock_guard<std::mutex> holder{asset_bank_lock};
|
|
update_costs_locked_assets();
|
|
update_lru_locked_assets();
|
|
|
|
if (id.id >= id_count)
|
|
return false;
|
|
|
|
auto *candidate = asset_bank[id.id];
|
|
if (candidate->consumed != 0 || candidate->pending_consumed != 0)
|
|
return true;
|
|
|
|
uint64_t estimate = iface->estimate_cost_asset(candidate->id, *candidate->handle);
|
|
auto task = group.create_task();
|
|
task->set_task_class(TaskClass::Background);
|
|
task->set_fence_counter_signal(signal.get());
|
|
task->set_desc("asset-manager-instantiate-single");
|
|
iface->instantiate_asset(*this, task.get(), candidate->id, *candidate->handle);
|
|
candidate->pending_consumed = estimate;
|
|
candidate->last_used = timestamp;
|
|
total_consumed += estimate;
|
|
|
|
// We cannot increment the timestamp here, remember this for later.
|
|
// We hold a lock on the asset bank here, so this is fine even if called concurrently.
|
|
blocking_signals++;
|
|
|
|
return true;
|
|
}
|
|
|
|
void AssetManager::iterate(ThreadGroup *group)
|
|
{
|
|
if (!iface)
|
|
return;
|
|
|
|
timestamp += blocking_signals;
|
|
blocking_signals = 0;
|
|
|
|
// If there is too much pending work in flight, skip.
|
|
uint64_t current_count = signal->get_count();
|
|
if (current_count + 3 < timestamp)
|
|
{
|
|
iface->latch_handles();
|
|
LOGI("Asset manager skipping iteration due to too much pending work.\n");
|
|
return;
|
|
}
|
|
|
|
TaskGroupHandle task;
|
|
if (group)
|
|
{
|
|
task = group->create_task();
|
|
task->set_desc("asset-manager-instantiate");
|
|
task->set_fence_counter_signal(signal.get());
|
|
task->set_task_class(TaskClass::Background);
|
|
}
|
|
else
|
|
signal->signal_increment();
|
|
|
|
std::lock_guard<std::mutex> holder{asset_bank_lock};
|
|
update_costs_locked_assets();
|
|
update_lru_locked_assets();
|
|
|
|
memcpy(sorted_assets.data(), asset_bank.data(), id_count * sizeof(sorted_assets[0]));
|
|
std::sort(sorted_assets.data(), sorted_assets.data() + id_count, [](const AssetInfo *a, const AssetInfo *b) -> bool {
|
|
// High prios come first since they will be activated.
|
|
// Then we sort by LRU.
|
|
// High consumption should be moved last, so they are candidates to be paged out if we're over budget.
|
|
// High pending consumption should be moved early since we don't want to page out resources that
|
|
// are in the middle of being loaded anyway.
|
|
// Finally, the ID is used as a tie breaker.
|
|
|
|
if (a->prio != b->prio)
|
|
return a->prio > b->prio;
|
|
else if (a->last_used != b->last_used)
|
|
return a->last_used > b->last_used;
|
|
else if (a->consumed != b->consumed)
|
|
return a->consumed < b->consumed;
|
|
else if (a->pending_consumed != b->pending_consumed)
|
|
return a->pending_consumed > b->pending_consumed;
|
|
else
|
|
return a->id.id < b->id.id;
|
|
});
|
|
|
|
size_t release_index = id_count;
|
|
uint64_t activated_cost_this_iteration = 0;
|
|
unsigned activation_count = 0;
|
|
size_t activate_index = 0;
|
|
|
|
// Aim to activate resources as long as we're in budget.
|
|
// Activate in order from highest priority to lowest.
|
|
bool can_activate = true;
|
|
while (can_activate &&
|
|
total_consumed < transfer_budget &&
|
|
activated_cost_this_iteration < transfer_budget_per_iteration &&
|
|
activate_index != release_index)
|
|
{
|
|
auto *candidate = sorted_assets[activate_index];
|
|
if (candidate->prio <= 0)
|
|
break;
|
|
|
|
// This resource is already active.
|
|
if (candidate->consumed != 0 || candidate->pending_consumed != 0)
|
|
{
|
|
activate_index++;
|
|
continue;
|
|
}
|
|
|
|
uint64_t estimate = iface->estimate_cost_asset(candidate->id, *candidate->handle);
|
|
|
|
can_activate = (total_consumed + estimate <= transfer_budget) || (candidate->prio >= persistent_prio());
|
|
while (!can_activate && activate_index + 1 != release_index)
|
|
{
|
|
auto *release_candidate = sorted_assets[--release_index];
|
|
if (release_candidate->consumed)
|
|
{
|
|
LOGI("Releasing ID %u due to page-in pressure.\n", release_candidate->id.id);
|
|
iface->release_asset(release_candidate->id);
|
|
total_consumed -= release_candidate->consumed;
|
|
release_candidate->consumed = 0;
|
|
}
|
|
can_activate = total_consumed + estimate <= transfer_budget;
|
|
}
|
|
|
|
if (can_activate)
|
|
{
|
|
// We're trivially in budget.
|
|
iface->instantiate_asset(*this, task.get(), candidate->id, *candidate->handle);
|
|
activation_count++;
|
|
|
|
candidate->pending_consumed = estimate;
|
|
total_consumed += estimate;
|
|
// Let this run over budget once.
|
|
// Ensures we can make forward progress no matter what the limit is.
|
|
activated_cost_this_iteration += estimate;
|
|
activate_index++;
|
|
}
|
|
}
|
|
|
|
// If we're 75% of budget, start garbage collecting non-resident resources ahead of time.
|
|
const uint64_t low_image_budget = (transfer_budget * 3) / 4;
|
|
|
|
const auto should_release = [&]() -> bool {
|
|
if (release_index == activate_index)
|
|
return false;
|
|
if (sorted_assets[release_index - 1]->prio == persistent_prio())
|
|
return false;
|
|
|
|
if (total_consumed > transfer_budget)
|
|
return true;
|
|
else if (total_consumed > low_image_budget && sorted_assets[release_index - 1]->prio == 0)
|
|
return true;
|
|
|
|
return false;
|
|
};
|
|
|
|
// If we're over budget, deactivate resources.
|
|
while (should_release())
|
|
{
|
|
auto *candidate = sorted_assets[--release_index];
|
|
if (candidate->consumed)
|
|
{
|
|
LOGI("Releasing 0-prio ID %u due to page-in pressure.\n", candidate->id.id);
|
|
iface->release_asset(candidate->id);
|
|
total_consumed -= candidate->consumed;
|
|
candidate->consumed = 0;
|
|
candidate->last_used = 0;
|
|
}
|
|
}
|
|
|
|
if (activated_cost_this_iteration)
|
|
{
|
|
LOGI("Activated %u resources for %llu KiB.\n", activation_count,
|
|
static_cast<unsigned long long>(activated_cost_this_iteration / 1024));
|
|
}
|
|
|
|
iface->latch_handles();
|
|
timestamp++;
|
|
}
|
|
}
|