mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-26 22:52:47 +00:00
feat(ugc): denoise=oidn traces icons' occlusion per pixel and denoises it (optional build)
Intel Open Image Denoise 2 (Apache-2.0) behind the CMake option DLU_OIDN (off): an installed OIDN is used when found, else Intel's release package (pinned by hash) is downloaded and its libraries copied next to the servers. A denoiser only removes noise that differs from pixel to pixel; the icons' occlusion comes from the bake, per vertex, which it leaves as it is (checked: white noise 0.17 -> 0.006 relative spread, per-vertex blocks unchanged). So with denoise=oidn a model's icon is drawn from model.noao.nif (its colors before the bake) with its occlusion traced per pixel of the supersampled image (denoise_samples rays, default 4, with the bake's distance and strength and the ray backend), box filtered and denoised at the icon's size, guided by the colors and normals. The model keeps its baked occlusion. Icons drawn again from stored files use the stored model.noao.nif the same way. OIDN works on a thread of its own; its time is charged to the job's thread (UgcThrottle::Charge), so the CPU budget and the recorded CPU time include it. Check: configure with -DDLU_OIDN=ON; UgcServer --make-model x.lxfml out oidn and compare its icon.png with one made with off; an OFF build leaves icons as they were. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -36,8 +36,9 @@ namespace UgcJobs {
|
||||
|
||||
namespace {
|
||||
// The icon files of a model, and false when nothing was drawn
|
||||
bool AddIcon(UgcStorage::Files& files, const UgcModel::Model& model, const UgcRender::IconOptions& options, const std::vector<float>* ao = nullptr) {
|
||||
const auto icon = UgcRender::RenderIcon(model, options, ao);
|
||||
bool AddIcon(UgcStorage::Files& files, const UgcModel::Model& model, const UgcRender::IconOptions& options, const std::vector<float>* ao = nullptr,
|
||||
const UgcModel::Model* plain = nullptr) {
|
||||
const auto icon = UgcRender::RenderIcon(model, options, ao, plain);
|
||||
bool drawn = false;
|
||||
for (size_t i = 3; i < icon.rgba.size(); i += 4) drawn = drawn || icon.rgba[i] != 0;
|
||||
files["icon.png"] = UgcFormats::EncodePng(icon);
|
||||
@@ -150,10 +151,16 @@ namespace UgcJobs {
|
||||
}
|
||||
|
||||
bool IconFromNif(const std::string& nif, const UgcRender::IconOptions& options, UgcStorage::Files& files, std::string& error,
|
||||
const std::map<int32_t, UgcModel::eLook>& tagLooks, const std::set<int32_t>& overlayTags) {
|
||||
const std::map<int32_t, UgcModel::eLook>& tagLooks, const std::set<int32_t>& overlayTags, const std::string* plainNif) {
|
||||
const auto readBack = NifFile::Parse(nif, 0, error);
|
||||
if (!readBack) return false;
|
||||
AddIcon(files, UgcModel::FromNif(*readBack, tagLooks, overlayTags), options);
|
||||
// The denoiser's guide: the model before its occlusion was baked in (model.noao.nif), when there is one
|
||||
std::optional<UgcModel::Model> plain;
|
||||
if (options.denoise != UgcRender::eDenoise::OFF && plainNif) {
|
||||
std::string plainError;
|
||||
if (const auto plainRead = NifFile::Parse(*plainNif, 0, plainError)) plain = UgcModel::FromNif(*plainRead, tagLooks, overlayTags);
|
||||
}
|
||||
AddIcon(files, UgcModel::FromNif(*readBack, tagLooks, overlayTags), options, nullptr, plain ? &*plain : nullptr);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -342,10 +349,12 @@ namespace UgcJobs {
|
||||
// Stored compressed only (the client downloads .gz; the dashboard's copies are inflated when asked for). The
|
||||
// LXFML is served from the database.
|
||||
AddDownload(outcome.files, "model.nif", nif);
|
||||
std::string plainNif;
|
||||
{
|
||||
const std::vector<LookPieces> opaque{ DivideByLook(preview.opaque, separate) };
|
||||
const std::vector<TransparentPieces> transparent{ transparentPieces[0] };
|
||||
outcome.files["model.noao.nif.gz"] = ZCompression::Gzip(UgcFormats::WriteLodNif("SceneNode_Model", groups(1, opaque, transparent)));
|
||||
plainNif = UgcFormats::WriteLodNif("SceneNode_Model", groups(1, opaque, transparent));
|
||||
outcome.files["model.noao.nif.gz"] = ZCompression::Gzip(plainNif);
|
||||
}
|
||||
|
||||
// The icon is drawn from the .nif just made (its most detailed LOD, read back like any client .nif), so it
|
||||
@@ -354,7 +363,7 @@ namespace UgcJobs {
|
||||
auto iconOptions = settings.icon;
|
||||
UgcIconParams::Apply(iconOptions, iconValues);
|
||||
std::string nifError;
|
||||
if (!IconFromNif(nif, iconOptions, outcome.files, nifError, settings.shaders.TagLooks(), settings.shaders.OverlayTags())) {
|
||||
if (!IconFromNif(nif, iconOptions, outcome.files, nifError, settings.shaders.TagLooks(), settings.shaders.OverlayTags(), &plainNif)) {
|
||||
outcome.error = "the .nif made can't be read back for the icon: " + nifError;
|
||||
return outcome;
|
||||
}
|
||||
|
||||
@@ -101,9 +101,10 @@ namespace UgcJobs {
|
||||
|
||||
// A model's icon files (icon.png, icon.dds download) drawn from its .nif (LOD 0), its metal and glow groups by
|
||||
// `tagLooks` (Shaders::TagLooks); false (and `error`) when the .nif can't be read
|
||||
// (the groups drawn over others, `overlayTags` (Shaders::OverlayTags), left out)
|
||||
// (the groups drawn over others, `overlayTags` (Shaders::OverlayTags), left out). `plainNif`: its model.noao.nif, the
|
||||
// denoiser's guide when options.denoise is on.
|
||||
bool IconFromNif(const std::string& nif, const UgcRender::IconOptions& options, UgcStorage::Files& files, std::string& error,
|
||||
const std::map<int32_t, UgcModel::eLook>& tagLooks = {}, const std::set<int32_t>& overlayTags = {});
|
||||
const std::map<int32_t, UgcModel::eLook>& tagLooks = {}, const std::set<int32_t>& overlayTags = {}, const std::string* plainNif = nullptr);
|
||||
|
||||
/**
|
||||
* A model's stats.json after its icon was drawn again in `iconMs`: ms.icon becomes that and ms.total changes by the
|
||||
|
||||
@@ -242,7 +242,9 @@ void UgcProcessor::Worker() {
|
||||
const auto nif = m_Storage.ReadNif(Kind::MODEL, job.id, "model.nif");
|
||||
auto options = settings.icon;
|
||||
UgcIconParams::Apply(options, job.iconValues);
|
||||
outcome.ok = nif && UgcJobs::IconFromNif(*nif, options, outcome.files, outcome.error, settings.shaders.TagLooks(), settings.shaders.OverlayTags());
|
||||
const auto plain = options.denoise != UgcRender::eDenoise::OFF ? m_Storage.ReadNif(Kind::MODEL, job.id, "model.noao.nif") : std::nullopt;
|
||||
outcome.ok = nif && UgcJobs::IconFromNif(*nif, options, outcome.files, outcome.error, settings.shaders.TagLooks(), settings.shaders.OverlayTags(),
|
||||
plain ? &*plain : nullptr);
|
||||
if (!nif) outcome.error = "the model has no stored .nif yet";
|
||||
}
|
||||
if (outcome.ok && outcome.files.contains("assembly.nif")) {
|
||||
@@ -279,7 +281,7 @@ void UgcProcessor::Worker() {
|
||||
continue;
|
||||
}
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
const double cpuStart = UgcThrottle::ThreadCpuSeconds();
|
||||
const double cpuStart = UgcThrottle::JobCpuSeconds();
|
||||
Done done{ job.kind, job.id, job.attempts };
|
||||
done.memoryEstimate = job.memory;
|
||||
done.iconOnly = job.iconOnly;
|
||||
@@ -290,7 +292,9 @@ void UgcProcessor::Worker() {
|
||||
auto options = settings.icon;
|
||||
UgcIconParams::Apply(options, job.iconValues);
|
||||
const auto iconStart = std::chrono::steady_clock::now();
|
||||
done.outcome.ok = nif && UgcJobs::IconFromNif(*nif, options, done.outcome.files, done.outcome.error, settings.shaders.TagLooks(), settings.shaders.OverlayTags());
|
||||
const auto plain = options.denoise != UgcRender::eDenoise::OFF ? m_Storage.ReadNif(Kind::MODEL, job.id, "model.noao.nif") : std::nullopt;
|
||||
done.outcome.ok = nif && UgcJobs::IconFromNif(*nif, options, done.outcome.files, done.outcome.error, settings.shaders.TagLooks(), settings.shaders.OverlayTags(),
|
||||
plain ? &*plain : nullptr);
|
||||
if (!nif) done.outcome.error = "no stored .nif";
|
||||
// The make's time keeps its icon's: the stats get the new icon's time, the row the difference (Collect)
|
||||
const auto stats = done.outcome.ok ? m_Storage.ReadNif(Kind::MODEL, job.id, "stats.json") : std::nullopt;
|
||||
@@ -336,7 +340,7 @@ void UgcProcessor::Worker() {
|
||||
}
|
||||
done.outcome.files.clear();
|
||||
done.milliseconds = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count();
|
||||
done.cpuMilliseconds = std::max(0.0, UgcThrottle::ThreadCpuSeconds() - cpuStart) * 1000.0;
|
||||
done.cpuMilliseconds = std::max(0.0, UgcThrottle::JobCpuSeconds() - cpuStart) * 1000.0;
|
||||
try {
|
||||
UgcThrottle::Checkpoint();
|
||||
} catch (const UgcThrottle::Cancelled&) {
|
||||
|
||||
@@ -22,6 +22,7 @@ namespace {
|
||||
std::atomic<int64_t> g_LastSleep{ 0 };
|
||||
|
||||
thread_local double t_LastCpu = -1.0;
|
||||
thread_local double t_Charged = 0.0; // CPU seconds libraries used for this thread on threads of their own
|
||||
|
||||
int64_t UnixMs() {
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
@@ -55,6 +56,17 @@ namespace UgcThrottle {
|
||||
return std::chrono::duration<double>(std::chrono::steady_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
|
||||
void Charge(double seconds) {
|
||||
if (!(seconds > 0.0)) return;
|
||||
t_Charged += seconds;
|
||||
// The next Checkpoint sees it as used since the last
|
||||
if (t_LastCpu >= 0.0) t_LastCpu -= seconds;
|
||||
}
|
||||
|
||||
double JobCpuSeconds() {
|
||||
return ThreadCpuSeconds() + t_Charged;
|
||||
}
|
||||
|
||||
void Begin() {
|
||||
t_LastCpu = ThreadCpuSeconds();
|
||||
}
|
||||
|
||||
@@ -36,6 +36,12 @@ namespace UgcThrottle {
|
||||
// This thread's CPU time in seconds
|
||||
double ThreadCpuSeconds();
|
||||
|
||||
// CPU time a library spent for this thread on threads of its own (Open Image Denoise's), counted as this thread's:
|
||||
// by the budget at the next Checkpoint and in JobCpuSeconds
|
||||
void Charge(double seconds);
|
||||
// This thread's CPU time with what was charged to it, in seconds (a job's CPU time is the difference)
|
||||
double JobCpuSeconds();
|
||||
|
||||
// Parses "18-23" (from 18:00 to 23:59; may wrap past midnight, "22-6") into from and to; false when it isn't that
|
||||
bool ParseHours(const std::string& text, int& from, int& to);
|
||||
// Whether `hour` (0-23) is inside the range; never when from or to is negative
|
||||
|
||||
Reference in New Issue
Block a user