mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-19 19:22:49 +00:00
fix(ugc): transparent bricks drawn see-through in mixed models; dUgcServer in folders by role
- The client puts a shape in its sorted, blended pass only when its NiMaterialProperty alpha is under 0.99999 (ShaderCommon::GetAlphaFlags 0x0109f5a0; the NiAlphaProperty blend flag isn't read); at 1.0 it's drawn solid with blending off. Transparent (and transparent glitter) shapes now get a material with alpha 0.9999, as the S01_Alpha shapes of the game's own brick models (res/BrickModels/ndmade) do; opaque shapes keep 1.0. Models with a transparent brick change; the others are byte for byte the same. - dUgcServer's files move into Bricks/, Model/, Render/, Formats/ and Processing/ (the CMakeLists says what each holds); includes are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
388
dUgcServer/Processing/UgcJobs.cpp
Normal file
388
dUgcServer/Processing/UgcJobs.cpp
Normal file
@@ -0,0 +1,388 @@
|
||||
#include "UgcJobs.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <sstream>
|
||||
|
||||
#include "json.hpp"
|
||||
|
||||
#include "NifFile.h"
|
||||
#include "Sd0.h"
|
||||
#include "UgcFormats.h"
|
||||
#include "UgcModel.h"
|
||||
#include "UgcModular.h"
|
||||
#include "ZCompression.h"
|
||||
|
||||
namespace UgcJobs {
|
||||
void AddDownload(UgcStorage::Files& files, const std::string& name, const std::string& data) {
|
||||
files[name + ".gz"] = ZCompression::Gzip(data);
|
||||
files[name + ".checksum"] = UgcFormats::ChecksumXml(data);
|
||||
// Without 3D services the client downloads it as sd0 (after asking its world for the checksum)
|
||||
auto sd0 = Sd0::Compress(data);
|
||||
if (!sd0.empty()) files[name + ".sd0"] = std::move(sd0);
|
||||
}
|
||||
|
||||
std::string LxfmlFromBlob(const std::string& blob) {
|
||||
if (blob.starts_with("<?xml") || blob.starts_with("<LXFML")) return blob;
|
||||
std::stringstream stream(blob);
|
||||
try {
|
||||
Sd0 sd0(stream);
|
||||
return sd0.GetAsStringUncompressed();
|
||||
} catch (...) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
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 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);
|
||||
AddDownload(files, "icon.dds", UgcFormats::EncodeDds(icon));
|
||||
return drawn;
|
||||
}
|
||||
}
|
||||
|
||||
size_t CountParts(std::string_view lxfml) {
|
||||
size_t count = 0;
|
||||
for (size_t at = lxfml.find("<Part"); at != std::string_view::npos; at = lxfml.find("<Part", at + 5)) count++;
|
||||
return count;
|
||||
}
|
||||
|
||||
uint64_t EstimateMemory(size_t parts, const Settings& settings) {
|
||||
// Measured: the icon's buffers, and per brick its mesh in each LOD (positions, normals, colors, indices,
|
||||
// the occlusion tree and copies made along the way), about 40 KB at LOD 0, and the hidden faces' ray tree
|
||||
// (about 60 bytes a triangle, one LOD at a time)
|
||||
const uint64_t icon = static_cast<uint64_t>(settings.icon.size) * settings.icon.supersample;
|
||||
const uint64_t fixed = icon * icon * 20 + 1024 * 1024 * 4 + 16 * 1024 * 1024;
|
||||
return fixed + static_cast<uint64_t>(parts) * (40 * 1024 * (1 + settings.lods.size()) + 16 * 1024);
|
||||
}
|
||||
|
||||
namespace {
|
||||
double Since(std::chrono::steady_clock::time_point start) {
|
||||
return std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count();
|
||||
}
|
||||
|
||||
// An opaque mesh's pieces by look ([eLook]: UgcModel::Divide's pieces), the looks without a shader of their own
|
||||
// with the plastic ones
|
||||
using LookPieces = std::array<std::vector<UgcModel::Mesh>, UgcModel::LOOK_COUNT>;
|
||||
LookPieces DivideByLook(const UgcModel::Mesh& mesh, const std::array<bool, UgcModel::LOOK_COUNT>& separate) {
|
||||
LookPieces pieces;
|
||||
const auto split = UgcModel::SplitLooks(mesh, separate);
|
||||
if (!split) {
|
||||
pieces[0] = UgcModel::Divide(mesh);
|
||||
return pieces;
|
||||
}
|
||||
for (size_t look = 0; look < UgcModel::LOOK_COUNT; look++) pieces[look] = UgcModel::Divide((*split)[look]);
|
||||
return pieces;
|
||||
}
|
||||
|
||||
// A model's transparent bricks as the .nif's shapes: one per brick (or all together, `combine`), the glitter
|
||||
// ones apart when `glitterApart`
|
||||
std::array<std::vector<UgcModel::Mesh>, 2> DivideTransparent(const UgcModel::Model& model, bool combine, bool glitterApart) {
|
||||
std::array<std::vector<UgcModel::Mesh>, 2> out;
|
||||
std::array<bool, UgcModel::LOOK_COUNT> separate{};
|
||||
separate[static_cast<size_t>(UgcModel::eLook::GLITTER)] = glitterApart;
|
||||
const auto glitter = static_cast<size_t>(UgcModel::eLook::GLITTER);
|
||||
if (combine) {
|
||||
const auto split = UgcModel::SplitLooks(model.transparent, separate);
|
||||
if (!split) {
|
||||
out[0] = UgcModel::Divide(model.transparent);
|
||||
} else {
|
||||
out[0] = UgcModel::Divide((*split)[0]);
|
||||
out[1] = UgcModel::Divide((*split)[glitter]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
for (auto& piece : UgcModel::SplitAt(model.transparent, model.transparentBricks)) {
|
||||
const auto split = UgcModel::SplitLooks(piece, separate);
|
||||
if (!split) {
|
||||
out[0].push_back(std::move(piece));
|
||||
continue;
|
||||
}
|
||||
if (!(*split)[0].Empty()) out[0].push_back(std::move((*split)[0]));
|
||||
if (!(*split)[glitter].Empty()) out[1].push_back(std::move((*split)[glitter]));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t Shaders::TagOf(UgcModel::eLook look) const {
|
||||
switch (look) {
|
||||
case UgcModel::eLook::METAL: return metal;
|
||||
case UgcModel::eLook::BRUSHED: return brushed;
|
||||
case UgcModel::eLook::GLOW: return glow;
|
||||
case UgcModel::eLook::GLITTER: return glitter;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
std::map<int32_t, UgcModel::eLook> Shaders::TagLooks() const {
|
||||
std::map<int32_t, UgcModel::eLook> looks{ { 88, UgcModel::eLook::METAL }, { 89, UgcModel::eLook::BRUSHED }, { 46, UgcModel::eLook::GLOW },
|
||||
{ 21, UgcModel::eLook::GLITTER } };
|
||||
for (const auto look : { UgcModel::eLook::METAL, UgcModel::eLook::BRUSHED, UgcModel::eLook::GLOW, UgcModel::eLook::GLITTER }) {
|
||||
if (const auto tag = TagOf(look); tag != 0) looks[static_cast<int32_t>(tag)] = look;
|
||||
}
|
||||
return looks;
|
||||
}
|
||||
|
||||
std::string ShapeName(const Settings& settings, UgcModel::eLook look, bool transparent) {
|
||||
if (transparent && look != UgcModel::eLook::GLITTER) return "S01_Alpha_Model";
|
||||
if (look == UgcModel::eLook::PLASTIC) return ("S" + settings.shaderOpaque + "_Opaque_Model").substr(0, 60);
|
||||
const auto tag = std::to_string(settings.shaders.TagOf(look));
|
||||
const char* name = look == UgcModel::eLook::METAL ? "_Metal_Model" : look == UgcModel::eLook::BRUSHED ? "_Brushed_Model" :
|
||||
look == UgcModel::eLook::GLOW ? "_Glow_Model" : transparent ? "_GlitterAlpha_Model" : "_Glitter_Model";
|
||||
return ("S" + std::string(tag.size() < 2 ? "0" : "") + tag + name).substr(0, 60);
|
||||
}
|
||||
|
||||
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 auto readBack = NifFile::Parse(nif, 0, error);
|
||||
if (!readBack) return false;
|
||||
AddIcon(files, UgcModel::FromNif(*readBack, tagLooks), options);
|
||||
return true;
|
||||
}
|
||||
|
||||
Outcome ProcessModel(const std::string& blob, UgcBricks::BrickLibrary& library, const Settings& settings, uint64_t seed, const UgcIconParams::Values& iconValues) {
|
||||
Outcome outcome;
|
||||
const auto started = std::chrono::steady_clock::now();
|
||||
const auto lxfml = LxfmlFromBlob(blob);
|
||||
if (lxfml.empty()) {
|
||||
outcome.error = "the stored LXFML can't be read";
|
||||
return outcome;
|
||||
}
|
||||
std::string error;
|
||||
const auto parts = UgcModel::ParseLxfml(lxfml, error);
|
||||
if (parts.empty()) {
|
||||
outcome.error = error;
|
||||
// A model with no bricks has nothing to make; the LXFML itself is still served
|
||||
outcome.empty = UgcModel::HasNoBricks(lxfml);
|
||||
return outcome;
|
||||
}
|
||||
if (settings.maxBricks > 0 && parts.size() > settings.maxBricks) {
|
||||
outcome.error = "the model has " + std::to_string(parts.size()) + " bricks, more than max_model_bricks (" + std::to_string(settings.maxBricks) + ")";
|
||||
return outcome;
|
||||
}
|
||||
|
||||
auto lods = settings.lods;
|
||||
std::erase_if(lods, [](uint32_t lod) { return lod > 3; });
|
||||
std::sort(lods.begin(), lods.end());
|
||||
lods.erase(std::unique(lods.begin(), lods.end()), lods.end());
|
||||
if (lods.empty()) lods.push_back(0);
|
||||
const auto ranges = UgcModel::LodRanges(lods, settings.lodDistances);
|
||||
|
||||
nlohmann::json stats;
|
||||
stats["version"] = 1;
|
||||
stats["bricks"] = parts.size();
|
||||
auto& lodStats = stats["lods"] = nlohmann::json::array();
|
||||
double buildMs = 0, hsrMs = 0, aoMs = 0;
|
||||
|
||||
// Every LOD made like LU Toolbox makes each LOD collection: colored, hidden faces removed, lighting baked, divided
|
||||
std::vector<UgcModel::Model> models;
|
||||
std::vector<LookPieces> opaquePieces;
|
||||
// Per level: the transparent bricks' pieces, plastic and (with the glitter group on) glitter
|
||||
using TransparentPieces = std::array<std::vector<UgcModel::Mesh>, 2>;
|
||||
std::vector<TransparentPieces> transparentPieces;
|
||||
// The looks with a shader of their own (UgcJobs::Shaders), each an NiLODNode apart from the plastic
|
||||
std::array<bool, UgcModel::LOOK_COUNT> separate{};
|
||||
for (size_t look = 1; look < UgcModel::LOOK_COUNT; look++) separate[look] = settings.shaders.TagOf(static_cast<UgcModel::eLook>(look)) != 0;
|
||||
const bool glowApart = separate[static_cast<size_t>(UgcModel::eLook::GLOW)];
|
||||
UgcModel::Model preview; // LOD 0 before the lighting bake, for the dashboard
|
||||
for (size_t i = 0; i < lods.size(); i++) {
|
||||
auto options = settings.build;
|
||||
options.seed = seed;
|
||||
options.lod = lods[i];
|
||||
auto step = std::chrono::steady_clock::now();
|
||||
auto model = UgcModel::Build(parts, library, options);
|
||||
buildMs += Since(step);
|
||||
if (i == 0) {
|
||||
if (!model.missingDesigns.empty()) {
|
||||
outcome.note = "no geometry for design(s)";
|
||||
for (const auto design : model.missingDesigns) outcome.note += " " + std::to_string(design);
|
||||
stats["missingDesigns"] = model.missingDesigns;
|
||||
}
|
||||
if (model.Empty()) {
|
||||
outcome.error = "none of the model's bricks have geometry";
|
||||
if (!outcome.note.empty()) outcome.error += " (" + outcome.note + ")";
|
||||
return outcome;
|
||||
}
|
||||
}
|
||||
nlohmann::json entry{ { "lod", lods[i] }, { "near", ranges[i].first }, { "far", ranges[i].second },
|
||||
{ "opaqueBefore", model.opaque.TriangleCount() }, { "transparent", model.transparent.TriangleCount() } };
|
||||
step = std::chrono::steady_clock::now();
|
||||
auto hsr = settings.hsr;
|
||||
hsr.seed = seed;
|
||||
const auto optimized = UgcHsr::RemoveHiddenFaces(model, hsr);
|
||||
hsrMs += Since(step);
|
||||
if (i == 0 && optimized.trianglesRemoved > 0) {
|
||||
if (!outcome.note.empty()) outcome.note += "; ";
|
||||
outcome.note += "removed " + std::to_string(optimized.trianglesRemoved) + " of " + std::to_string(optimized.trianglesBefore) + " triangles";
|
||||
}
|
||||
if (i == 0) preview = model;
|
||||
// The emissive shader lerps from its own lighting to the vertex color, so glowing bricks keep their plain
|
||||
// color: no occlusion, and no glow added (it would glow twice)
|
||||
std::vector<glm::vec4> plainColors;
|
||||
if (glowApart && !model.opaque.looks.empty()) plainColors = model.opaque.colors;
|
||||
step = std::chrono::steady_clock::now();
|
||||
UgcRender::BakeAo(model, settings.ao);
|
||||
aoMs += Since(step);
|
||||
for (size_t v = 0; v < plainColors.size() && v < model.opaque.looks.size(); v++) {
|
||||
if (model.opaque.looks[v] == UgcModel::eLook::GLOW) model.opaque.colors[v] = glm::vec4(glm::vec3(plainColors[v]), 1.0f);
|
||||
}
|
||||
entry["opaqueAfter"] = model.opaque.TriangleCount();
|
||||
entry["vertices"] = model.opaque.positions.size() + model.transparent.positions.size();
|
||||
opaquePieces.push_back(DivideByLook(model.opaque, separate));
|
||||
transparentPieces.push_back(DivideTransparent(model, settings.combineTransparent, separate[static_cast<size_t>(UgcModel::eLook::GLITTER)]));
|
||||
size_t shapes = transparentPieces.back()[0].size() + transparentPieces.back()[1].size();
|
||||
for (const auto& pieces : opaquePieces.back()) shapes += pieces.size();
|
||||
entry["shapes"] = shapes;
|
||||
// Triangles per group (NiLODNode) when metal, glow or glitter have groups of their own
|
||||
if (std::find(separate.begin(), separate.end(), true) != separate.end()) {
|
||||
auto& byGroup = entry["groups"] = nlohmann::json::object();
|
||||
for (size_t look = 0; look < UgcModel::LOOK_COUNT; look++) {
|
||||
size_t triangles = 0;
|
||||
for (const auto& piece : opaquePieces.back()[look]) triangles += piece.TriangleCount();
|
||||
if (triangles > 0) byGroup[ShapeName(settings, static_cast<UgcModel::eLook>(look), false)] = triangles;
|
||||
}
|
||||
for (size_t kind = 0; kind < 2; kind++) {
|
||||
size_t triangles = 0;
|
||||
for (const auto& piece : transparentPieces.back()[kind]) triangles += piece.TriangleCount();
|
||||
if (triangles > 0) byGroup[ShapeName(settings, kind ? UgcModel::eLook::GLITTER : UgcModel::eLook::PLASTIC, true)] = triangles;
|
||||
}
|
||||
}
|
||||
lodStats.push_back(entry);
|
||||
}
|
||||
outcome.aoBaked = settings.ao.enabled;
|
||||
|
||||
// An NiLODNode for the opaque bricks and one for the transparent ones, as LU Toolbox names them, and one for
|
||||
// each look with a shader of its own between them. Every group has every level (empty where it has nothing).
|
||||
// Transparent glitter last, after the plain transparent bricks.
|
||||
const auto groups = [&](size_t levels, const std::vector<LookPieces>& opaque, const std::vector<TransparentPieces>& transparent) {
|
||||
std::vector<UgcFormats::NifLodGroup> out;
|
||||
for (size_t kind = 0; kind < UgcModel::LOOK_COUNT + 2; kind++) {
|
||||
const bool isTransparent = kind >= UgcModel::LOOK_COUNT;
|
||||
const bool glitter = kind == UgcModel::LOOK_COUNT + 1 || kind == static_cast<size_t>(UgcModel::eLook::GLITTER);
|
||||
const auto look = glitter ? UgcModel::eLook::GLITTER : isTransparent ? UgcModel::eLook::PLASTIC : static_cast<UgcModel::eLook>(kind);
|
||||
UgcFormats::NifLodGroup group{ ShapeName(settings, look, isTransparent), isTransparent, {} };
|
||||
if (look == UgcModel::eLook::GLOW) group.emissive = std::max(settings.shaders.glowEmissive, 0.0f);
|
||||
if (glitter) group.glitter = &settings.shaders.glitterParams;
|
||||
bool any = false;
|
||||
for (size_t i = 0; i < levels; i++) {
|
||||
UgcFormats::NifLod lod{ ranges[i].first, ranges[i].second, "LOD_" + std::to_string(lods[i]), {} };
|
||||
for (const auto& piece : (isTransparent ? transparent[i][glitter ? 1 : 0] : opaque[i][kind])) lod.pieces.push_back(&piece);
|
||||
any = any || !lod.pieces.empty();
|
||||
group.lods.push_back(std::move(lod));
|
||||
}
|
||||
if (any) out.push_back(std::move(group));
|
||||
}
|
||||
return out;
|
||||
};
|
||||
const auto nif = UgcFormats::WriteLodNif("SceneNode_Model", groups(lods.size(), opaquePieces, transparentPieces));
|
||||
// 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);
|
||||
{
|
||||
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)));
|
||||
}
|
||||
|
||||
// The icon is drawn from the .nif just made (its most detailed LOD, read back like any client .nif), so it
|
||||
// shows what the game shows: the colors with their variation, hidden faces removed, the baked lighting.
|
||||
const auto iconStart = std::chrono::steady_clock::now();
|
||||
auto iconOptions = settings.icon;
|
||||
UgcIconParams::Apply(iconOptions, iconValues);
|
||||
std::string nifError;
|
||||
if (!IconFromNif(nif, iconOptions, outcome.files, nifError, settings.shaders.TagLooks())) {
|
||||
outcome.error = "the .nif made can't be read back for the icon: " + nifError;
|
||||
return outcome;
|
||||
}
|
||||
const double iconMs = Since(iconStart);
|
||||
|
||||
stats["ms"] = { { "build", std::lround(buildMs) }, { "hiddenSurfaces", std::lround(hsrMs) }, { "ambientOcclusion", std::lround(aoMs) },
|
||||
{ "icon", std::lround(iconMs) }, { "total", std::lround(Since(started)) } };
|
||||
stats["settings"] = { { "palette", settings.build.palette == UgcModel::ePalette::LU_TOOLBOX ? "lu_toolbox" : "brickdb" },
|
||||
{ "colorVariation", settings.build.colorVariation }, { "transparentOpacity", settings.build.transparentOpacity },
|
||||
{ "removeHiddenFaces", settings.hsr.enabled }, { "groundPlane", settings.hsr.groundPlane }, { "hsrSamples", settings.hsr.samples },
|
||||
{ "hsrBounces", settings.hsr.bounces }, { "hsrSampleSpacing", settings.hsr.spacing }, { "hsrMinPoints", settings.hsr.minPoints },
|
||||
{ "ao", settings.ao.enabled }, { "aoDistance", settings.ao.distance }, { "aoSamples", settings.ao.samples }, { "aoStrength", settings.ao.strength } };
|
||||
outcome.stats = stats.dump();
|
||||
outcome.files["stats.json"] = outcome.stats;
|
||||
outcome.ok = true;
|
||||
return outcome;
|
||||
}
|
||||
|
||||
std::optional<UgcModel::Model> AssembleModular(const ModularInput& input, const std::filesystem::path& res, glm::mat4& additionalRotation, std::string& error, std::string& note) {
|
||||
const auto build = UgcModular::ParseBuild(input.buildXml);
|
||||
if (!build) {
|
||||
error = "the build type has no topology in ModularBuildComponent";
|
||||
return std::nullopt;
|
||||
}
|
||||
additionalRotation = build->additionalRotation;
|
||||
std::vector<UgcModular::Module> modules;
|
||||
for (const auto& moduleInput : input.modules) {
|
||||
const auto path = UgcBricks::ResolvePath(res, moduleInput.renderAsset);
|
||||
const auto data = path ? UgcBricks::ReadFile(*path) : std::nullopt;
|
||||
if (!data) {
|
||||
note += "module " + std::to_string(moduleInput.lot) + " has no mesh (" + moduleInput.renderAsset + "); ";
|
||||
continue;
|
||||
}
|
||||
std::string nifError;
|
||||
auto nif = NifFile::Parse(*data, 0, nifError);
|
||||
if (!nif) {
|
||||
note += "module " + std::to_string(moduleInput.lot) + ": " + nifError + "; ";
|
||||
continue;
|
||||
}
|
||||
modules.push_back({ moduleInput.partCode, std::move(*nif), UgcModular::ParseModuleConnections(moduleInput.moduleXml) });
|
||||
}
|
||||
if (modules.empty()) {
|
||||
error = "none of the modules have a mesh";
|
||||
if (!note.empty()) error += " (" + note + ")";
|
||||
return std::nullopt;
|
||||
}
|
||||
auto model = UgcModular::Assemble(*build, modules, note);
|
||||
if (model.Empty()) {
|
||||
error = "the modules have no triangles";
|
||||
return std::nullopt;
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
Outcome ProcessModular(const ModularInput& input, const std::filesystem::path& res, const Settings& settings) {
|
||||
Outcome outcome;
|
||||
glm::mat4 additionalRotation{ 1.0f };
|
||||
const auto model = AssembleModular(input, res, additionalRotation, outcome.error, outcome.note);
|
||||
if (!model) return outcome;
|
||||
auto options = ModularIconOptions(input, settings);
|
||||
options.modelRotation = additionalRotation;
|
||||
AddIcon(outcome.files, *model, options);
|
||||
outcome.files["combo.json"] = nlohmann::json{ { "key", input.key }, { "buildType", input.buildType } }.dump();
|
||||
outcome.ok = true;
|
||||
return outcome;
|
||||
}
|
||||
|
||||
std::optional<std::string> AssemblyNif(const ModularInput& input, const std::filesystem::path& res, std::string& error) {
|
||||
glm::mat4 additionalRotation{ 1.0f };
|
||||
std::string note;
|
||||
auto model = AssembleModular(input, res, additionalRotation, error, note);
|
||||
if (!model) return std::nullopt;
|
||||
// Turned as the icon renderer turns it before the pose's own rotation, so the editor's model rotation starts from here
|
||||
model->opaque.Transform(additionalRotation);
|
||||
model->transparent.Transform(additionalRotation);
|
||||
const auto opaque = UgcModel::Split(model->opaque), transparent = UgcModel::Split(model->transparent);
|
||||
std::vector<UgcFormats::NifShape> shapes;
|
||||
for (const auto& piece : opaque) shapes.push_back({ "S01_Opaque_Model", &piece, false });
|
||||
for (const auto& piece : transparent) shapes.push_back({ "S01_Alpha_Model", &piece, true });
|
||||
return UgcFormats::WriteNif("SceneNode_Assembly", shapes);
|
||||
}
|
||||
|
||||
UgcRender::IconOptions ModularIconOptions(const ModularInput& input, const Settings& settings) {
|
||||
auto options = settings.icon;
|
||||
UgcIconParams::Apply(options, input.iconValues);
|
||||
return options;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
126
dUgcServer/Processing/UgcJobs.h
Normal file
126
dUgcServer/Processing/UgcJobs.h
Normal file
@@ -0,0 +1,126 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "UgcBricks.h"
|
||||
#include "UgcHsr.h"
|
||||
#include "UgcIconParams.h"
|
||||
#include "UgcRender.h"
|
||||
#include "UgcStorage.h"
|
||||
|
||||
/**
|
||||
* The work a UGC worker thread does for one item: everything from the stored LXFML (or a modular build's modules) to
|
||||
* the finished files. No database, network or CDClient: the main thread gathers the input and stores the result.
|
||||
*/
|
||||
namespace UgcJobs {
|
||||
/**
|
||||
* How models are made. The defaults are LU Toolbox's (Process Model, Bake Lighting and the icon renderer), see the
|
||||
* parity table in docs/UgcServer.md.
|
||||
*/
|
||||
/**
|
||||
* The shaders of the metal, glow and glitter colors (UgcModel::eLook): the mapShaders id each look's own NiLODNode
|
||||
* names (S88_Metal_Model, ...), 0 for none (the colors stay in S01_Opaque_Model, or S01_Alpha_Model when
|
||||
* transparent, as on live). Not what live did: live's models are all S01 (docs/UgcServer.md, "Metal and glow").
|
||||
*/
|
||||
struct Shaders {
|
||||
uint32_t metal{}; // shader_metal: 88 Polished Metal
|
||||
uint32_t brushed{}; // shader_brushed: 89 Brushed Steel
|
||||
uint32_t glow{}; // shader_glow: 46 LEGO-Emissive
|
||||
uint32_t glitter{}; // shader_glitter: 21 LEGO-AnimUV (opaque and transparent glitter, each a group)
|
||||
float glowEmissive{ 1.0f }; // glow_emissive: the glow shapes' NiMaterialProperty emissive (how much the vertex color shows unlit)
|
||||
UgcGlitter::Params glitterParams; // glitter_size, glitter_density, glitter_speed
|
||||
|
||||
// The mapShaders id of a look's group, 0 for the plastic S01_Opaque_Model
|
||||
uint32_t TagOf(UgcModel::eLook look) const;
|
||||
// Multishader tag -> look, for reading the looks back out of a .nif (the icon): these settings' ids, and the
|
||||
// client's Polished Metal (88), Brushed Steel (89), LEGO-Emissive (46) and LEGO-AnimUV (21) for .nifs made with
|
||||
// other settings
|
||||
std::map<int32_t, UgcModel::eLook> TagLooks() const;
|
||||
};
|
||||
|
||||
struct Settings {
|
||||
UgcModel::BuildOptions build; // palette, color variation, transparent opacity
|
||||
std::vector<uint32_t> lods{ 0, 2 }; // brickprimitives levels made (LU Toolbox imports LOD 0 and 2; the client has no 3)
|
||||
UgcModel::LodDistances lodDistances;
|
||||
std::string shaderOpaque{ "01" }; // S<shader>_Opaque_...; transparent shapes are always S01
|
||||
bool combineTransparent{ false }; // one shape for all transparent bricks, else one per brick (Combine Transparent)
|
||||
Shaders shaders; // metal and glow groups (all off by default)
|
||||
UgcHsr::Options hsr; // hidden surface removal (Remove Hidden Faces)
|
||||
UgcRender::AoOptions ao; // Bake Lighting (AO Only)
|
||||
UgcRender::IconOptions icon; // from the icon_* settings (UgcIconParams); presets and overrides go over it
|
||||
uint32_t maxBricks{}; // a model with more fails; 0: no limit
|
||||
};
|
||||
|
||||
struct Outcome {
|
||||
bool ok{};
|
||||
std::string error; // why it failed
|
||||
std::string note; // what was odd but not fatal (missing bricks, ...)
|
||||
UgcStorage::Files files; // name -> bytes, when ok
|
||||
bool aoBaked{};
|
||||
bool empty{}; // nothing to make (no bricks): not a failure
|
||||
std::string stats; // stats.json: bricks, triangles before and after per LOD, timings
|
||||
};
|
||||
|
||||
// The client's download of `data` (`name` + ".gz" and ".checksum") added to `files`
|
||||
void AddDownload(UgcStorage::Files& files, const std::string& name, const std::string& data);
|
||||
|
||||
// The LXFML of a ugc row's lxfml column (an sd0 stream, or plain LXFML); empty when it can't be read
|
||||
std::string LxfmlFromBlob(const std::string& blob);
|
||||
|
||||
// A player model: the optimized .nif, its icon and its LXFML for download, and stats.json. `seed` picks the color
|
||||
// variation's random numbers (the model's id, so making it again gives the same colors).
|
||||
// `iconValues`: the player models' preset and this model's override (UgcIconParams), over settings.icon.
|
||||
Outcome ProcessModel(const std::string& blob, UgcBricks::BrickLibrary& library, const Settings& settings, uint64_t seed = 0,
|
||||
const UgcIconParams::Values& iconValues = {});
|
||||
|
||||
// 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
|
||||
bool IconFromNif(const std::string& nif, const UgcRender::IconOptions& options, UgcStorage::Files& files, std::string& error,
|
||||
const std::map<int32_t, UgcModel::eLook>& tagLooks = {});
|
||||
|
||||
// The name of a group of shapes (its NiLODNode and shapes): S01_Opaque_Model, S01_Alpha_Model, S88_Metal_Model,
|
||||
// S89_Brushed_Model, S46_Glow_Model, S21_Glitter_Model and S21_GlitterAlpha_Model (transparent glitter; the ids from
|
||||
// the settings), at most 60 characters as LU Toolbox cuts them
|
||||
std::string ShapeName(const Settings& settings, UgcModel::eLook look, bool transparent);
|
||||
|
||||
// How many bricks (parts) an LXFML has, counted cheaply (for the memory estimate before a job starts)
|
||||
size_t CountParts(std::string_view lxfml);
|
||||
|
||||
// About how much memory making a model of `parts` bricks takes, in bytes
|
||||
uint64_t EstimateMemory(size_t parts, const Settings& settings);
|
||||
|
||||
struct ModuleInput {
|
||||
uint32_t lot{};
|
||||
uint32_t partCode{};
|
||||
std::string renderAsset; // RenderComponent.render_asset, relative to the client's res folder
|
||||
std::string moduleXml; // ModuleComponent.xml
|
||||
};
|
||||
|
||||
struct ModularInput {
|
||||
std::vector<ModuleInput> modules;
|
||||
std::string buildXml; // ModularBuildComponent.xml of the modules' build type
|
||||
int32_t buildType{}; // 3 rocket, 6 car
|
||||
std::string key; // the combination (UgcModularKey::Normalize)
|
||||
UgcIconParams::Values iconValues; // the build type's preset and the combination's override (DB), over the settings
|
||||
};
|
||||
|
||||
// The icon options for a car or rocket: the settings, then its build type's preset and the combination's override
|
||||
UgcRender::IconOptions ModularIconOptions(const ModularInput& input, const Settings& settings);
|
||||
|
||||
// A modular build's modules' meshes put together (UgcModular::Assemble), not yet turned by the build type's
|
||||
// AdditionalModelRotation (given back in `additionalRotation`); nullopt with `error` when there's nothing to draw
|
||||
std::optional<UgcModel::Model> AssembleModular(const ModularInput& input, const std::filesystem::path& res, glm::mat4& additionalRotation, std::string& error, std::string& note);
|
||||
|
||||
// A modular build's icon (icon.png, icon.dds download, combo.json), from its modules' meshes put together
|
||||
Outcome ProcessModular(const ModularInput& input, const std::filesystem::path& res, const Settings& settings);
|
||||
|
||||
// The assembled mesh as the icon renderer turns it (AdditionalModelRotation applied), as a .nif for the dashboard's
|
||||
// pose editor; nullopt with `error` when there's nothing to draw
|
||||
std::optional<std::string> AssemblyNif(const ModularInput& input, const std::filesystem::path& res, std::string& error);
|
||||
}
|
||||
1019
dUgcServer/Processing/UgcProcessor.cpp
Normal file
1019
dUgcServer/Processing/UgcProcessor.cpp
Normal file
File diff suppressed because it is too large
Load Diff
310
dUgcServer/Processing/UgcProcessor.h
Normal file
310
dUgcServer/Processing/UgcProcessor.h
Normal file
@@ -0,0 +1,310 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <filesystem>
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "DeferredReply.h"
|
||||
#include "dCommonVars.h"
|
||||
#include "json_fwd.hpp"
|
||||
#include "UgcBricks.h"
|
||||
#include "UgcIconParams.h"
|
||||
#include "UgcJobs.h"
|
||||
#include "UgcStorage.h"
|
||||
|
||||
/**
|
||||
* The UGC server's queue. The main thread (Update) takes pending rows from the database, gathers each one's input
|
||||
* (the stored LXFML, or a modular build's modules from the CDClient) and hands it to the worker threads; they make
|
||||
* and write the files; the main thread records the outcome in the database. Workers never touch the database, the
|
||||
* CDClient or the network.
|
||||
*/
|
||||
class UgcProcessor {
|
||||
public:
|
||||
using Kind = UgcStorage::Kind;
|
||||
|
||||
struct Config {
|
||||
uint32_t pollIntervalMs{ 2000 };
|
||||
uint32_t pollBatch{ 32 };
|
||||
uint32_t maxAttempts{ 3 };
|
||||
uint64_t maxStorageBytes{}; // 0: no limit
|
||||
size_t threads{ 2 };
|
||||
};
|
||||
|
||||
/**
|
||||
* What the workers may use; changed while running (Configure) when the settings are reloaded.
|
||||
*/
|
||||
struct Limits {
|
||||
double maxCpus{}; // CPUs the workers may use together on average (UgcThrottle); 0: no limit
|
||||
uint64_t maxMemoryBytes{}; // estimated memory of the jobs running at once; 0: no limit
|
||||
int nice{}; // the workers' scheduling priority (Linux nice, 0 to 19)
|
||||
int pauseFromHour{ -1 }; // local hours in which no new jobs start (from, to; -1: never)
|
||||
int pauseToHour{ -1 };
|
||||
};
|
||||
|
||||
UgcProcessor(Config config, UgcStorage& storage, UgcBricks::BrickLibrary& library, UgcJobs::Settings settings);
|
||||
~UgcProcessor();
|
||||
|
||||
void Start();
|
||||
|
||||
// Main thread: new settings and limits (config reload)
|
||||
void Configure(UgcJobs::Settings settings, Limits limits);
|
||||
// Waits for the jobs that are running; queued ones are dropped (they stay pending in the database)
|
||||
void Stop();
|
||||
|
||||
/**
|
||||
* Main thread, live updates: take no new work. Queued jobs are dropped (they stay pending in the database, so the
|
||||
* next UGC server makes them); the running ones finish and their outcome is recorded by Update as usual.
|
||||
*/
|
||||
void Drain();
|
||||
// Main thread: draining and nothing running or waiting to be recorded any more
|
||||
bool Drained() const;
|
||||
bool IsDraining() const { return m_Draining; }
|
||||
|
||||
// Main thread: poll, dispatch, record results, keep the storage under its cap
|
||||
void Update();
|
||||
|
||||
enum class Availability {
|
||||
READY, // the files are there
|
||||
QUEUED, // it exists and is being made (again)
|
||||
UNKNOWN, // no such item, or it failed
|
||||
};
|
||||
|
||||
// Main thread: whether an item's files can be served, queuing it to be made when it exists but they're missing
|
||||
Availability Request(Kind kind, LWOOBJID id);
|
||||
|
||||
// Main thread: the id an item's files are stored under. A car or rocket's are its combination's (every build of
|
||||
// the same modules shares one icon); 0 when a build's modules can't be told.
|
||||
LWOOBJID StorageId(Kind kind, LWOOBJID id);
|
||||
|
||||
/**
|
||||
* Main thread: renders an icon with `values` (UgcIconParams, over the settings) without storing it, on a worker
|
||||
* (within the budgets, ahead of the queue); `reply` gets the PNG. A player model's is drawn from its stored .nif, a
|
||||
* car or rocket's from `modules`. False (and `error`) when there's nothing to draw from.
|
||||
*/
|
||||
bool QueuePreview(Kind kind, LWOOBJID id, const std::string& modules, const UgcIconParams::Values& values, DeferredReply reply, std::string& error);
|
||||
|
||||
/**
|
||||
* Main thread: the assembled mesh of a module combination as a .nif (UgcJobs::AssemblyNif), for the dashboard's
|
||||
* pose editor: from a small cache of the last ones asked for, else made on a worker (within the budgets, ahead of
|
||||
* the queue) and cached. `reply` gets the .nif. False (and `error`) when the modules can't be told.
|
||||
*/
|
||||
bool QueueAssembly(const std::string& modules, DeferredReply reply, std::string& error);
|
||||
|
||||
// Main thread: queues every stored icon of a kind ("model", or "build<type>" for cars and rockets) to be drawn again
|
||||
// with the current settings, presets and overrides; only icons (models' from their stored .nif). How many.
|
||||
size_t RegenerateIcons(const std::string& kind);
|
||||
|
||||
enum class eAfterDelete {
|
||||
ON_DEMAND, // rows stay made; the files are made again when something asks for them
|
||||
NOW, // rows go back to waiting, so they're made again soon
|
||||
GONE, // rows are marked failed ("deleted"), so they aren't made again until someone asks for that
|
||||
};
|
||||
|
||||
struct DeleteRequest {
|
||||
Kind kind{};
|
||||
std::vector<LWOOBJID> ids; // models, or builds (their combinations are deleted); empty with `all`
|
||||
bool all{};
|
||||
int64_t olderThanDays{}; // only files made longer ago than this (0: any)
|
||||
int64_t unusedDays{}; // only files not asked for in this long (0: any)
|
||||
eAfterDelete after{ eAfterDelete::ON_DEMAND };
|
||||
};
|
||||
|
||||
struct DeleteResult {
|
||||
size_t deleted{};
|
||||
uint64_t bytes{};
|
||||
size_t busy{}; // being made right now, left alone
|
||||
std::vector<std::string> notes;
|
||||
bool started{}; // the files are being deleted in the background (PurgeStatus); `deleted` and `bytes` come later
|
||||
size_t queued{}; // items to look at (0 with `all`: every stored item of the kind)
|
||||
};
|
||||
|
||||
/**
|
||||
* Main thread: deletes stored files, on a background thread (a purge can be thousands of folders, and the main
|
||||
* thread serves downloads and the master link): the reply comes at once, and PurgeStatus follows it. Items being
|
||||
* made are skipped, and no new item is started until the purge is done, so a worker never writes what is being
|
||||
* deleted. One purge (or storage clean-up) at a time; another is refused while one runs.
|
||||
*/
|
||||
DeleteResult Delete(const DeleteRequest& request);
|
||||
|
||||
// Main thread: the running or last purge ({} when none ran): {state: running|saving|done, kind, checked, deleted,
|
||||
// bytes, started (Unix seconds), finished}
|
||||
nlohmann::json PurgeStatus() const;
|
||||
|
||||
// Main thread: what the server is doing, for /status
|
||||
nlohmann::json Status() const;
|
||||
|
||||
// Any thread: jobs waiting for a worker, and workers busy (traffic diagnostics)
|
||||
size_t Queued() const { std::lock_guard lock(m_Mutex); return m_Jobs.size(); }
|
||||
size_t Busy() const { std::lock_guard lock(m_Mutex); return m_Active; }
|
||||
size_t Threads() const { return m_Config.threads; }
|
||||
// Main thread: the process's CPU use (percent of one core) and resident memory, and the running jobs' estimate
|
||||
double CpuPercent() const { return m_CpuPercent; }
|
||||
static uint64_t ResidentBytes();
|
||||
uint64_t JobMemory() const { std::lock_guard lock(m_Mutex); return m_MemoryInUse; }
|
||||
bool Throttled() const;
|
||||
|
||||
// Main thread: totals since the start and the files' size (traffic reports, then the dashboard and /metrics)
|
||||
uint64_t Made() const { return m_Made; }
|
||||
uint64_t Failed() const { return m_Failed; }
|
||||
uint64_t Evicted() const { return m_Evicted; }
|
||||
uint64_t StoredBytes() const { return m_StoredBytes; }
|
||||
uint64_t MaxStorageBytes() const { return m_Config.maxStorageBytes; }
|
||||
|
||||
// Main thread: the models whose mesh (model.nif) was written with a different checksum than before since the last
|
||||
// call, for the worlds (UGC_MODELS_MADE); a model made again unchanged (e.g. after eviction) isn't listed
|
||||
std::vector<LWOOBJID> TakeChangedMeshes() { std::vector<LWOOBJID> ids; ids.swap(m_ChangedMeshes); return ids; }
|
||||
|
||||
private:
|
||||
struct Job {
|
||||
Kind kind{};
|
||||
LWOOBJID id{};
|
||||
uint32_t attempts{};
|
||||
std::string blob; // models: the LXFML
|
||||
UgcJobs::ModularInput modular; // modular builds
|
||||
uint64_t memory{}; // estimated bytes it needs
|
||||
size_t parts{};
|
||||
DeferredReply preview; // an icon preview: answered with the PNG, nothing stored
|
||||
bool iconOnly{}; // a model's icon drawn again from its stored .nif
|
||||
bool assembly{}; // with `preview`: answered with the assembled .nif instead of an icon
|
||||
UgcIconParams::Values iconValues; // models: the preset and override (UgcIconParams)
|
||||
bool priority{}; // models staff asked to be made again: queued at the front
|
||||
};
|
||||
|
||||
// A written file's MD5 and size as the client has it after inflating it (from its .checksum)
|
||||
struct Checksum {
|
||||
std::string file; // icon.dds, model.nif
|
||||
std::string md5;
|
||||
uint32_t size{};
|
||||
};
|
||||
|
||||
struct Done {
|
||||
Kind kind{};
|
||||
LWOOBJID id{};
|
||||
uint32_t attempts{};
|
||||
UgcJobs::Outcome outcome;
|
||||
uint64_t bytes{};
|
||||
double milliseconds{};
|
||||
double cpuMilliseconds{}; // the worker thread's CPU time for it
|
||||
uint64_t memoryEstimate{}; // the bytes it was estimated to need (the memory budget's figure)
|
||||
bool iconOnly{};
|
||||
std::vector<Checksum> checksums; // of the files written that the client downloads as sd0
|
||||
};
|
||||
|
||||
// Main thread: records the checksums of an item's files (ugc_file_checksums)
|
||||
void StoreChecksums(Kind kind, LWOOBJID storageId, const std::vector<Checksum>& checksums);
|
||||
|
||||
// Main thread: fills in what was made before the worlds answered manifest requests, a little per Update: the
|
||||
// icons' sd0 files and checksums of the items stored, and the builds' combination ids
|
||||
void Backfill();
|
||||
std::deque<UgcStorage::Entry> m_BackfillItems;
|
||||
bool m_BackfillItemsDone{ true };
|
||||
std::deque<LWOOBJID> m_StatsBackfill; // models whose triangle counts are read from their stats.json once
|
||||
bool m_StatsBackfillDone{ true };
|
||||
bool m_BackfillBuildsDone{ false };
|
||||
|
||||
void Poll();
|
||||
// Main thread: the icon values for a kind and an item (the kind's preset, then the item's override)
|
||||
UgcIconParams::Values IconValues(const std::string& kind, const std::string& itemTarget);
|
||||
void Collect();
|
||||
void Record(const Done& done);
|
||||
void Worker();
|
||||
void SampleUsage();
|
||||
|
||||
Config m_Config;
|
||||
UgcStorage& m_Storage;
|
||||
UgcBricks::BrickLibrary& m_Library;
|
||||
UgcJobs::Settings m_Settings; // guarded by m_Mutex (workers copy it per job)
|
||||
Limits m_Limits; // guarded by m_Mutex
|
||||
|
||||
mutable std::mutex m_Mutex;
|
||||
std::condition_variable m_Wake;
|
||||
std::deque<Job> m_Jobs;
|
||||
std::deque<Done> m_Done;
|
||||
size_t m_Active{};
|
||||
uint64_t m_MemoryInUse{}; // estimates of the running jobs
|
||||
uint64_t m_Waiting{}; // times a job waited for memory
|
||||
bool m_Paused{}; // main thread: in the pause hours
|
||||
// Main thread: process CPU use, measured between status reads
|
||||
std::chrono::steady_clock::time_point m_CpuSampled{};
|
||||
double m_CpuSeconds{};
|
||||
double m_CpuPercent{};
|
||||
bool m_Stopping{};
|
||||
bool m_Draining{}; // main thread: live update, no new work (Drain)
|
||||
std::vector<std::thread> m_Threads;
|
||||
|
||||
// Deleting stored folders off the main thread: a purge (Delete) or the storage cap (Evict). The thread only touches
|
||||
// the filesystem; the main thread applies what it removed (Update -> CollectFileTask).
|
||||
enum class eFileTask { NONE, PURGE, EVICT };
|
||||
struct Removed {
|
||||
LWOOBJID storageId{};
|
||||
uint64_t bytes{};
|
||||
};
|
||||
void StartFileTask(eFileTask type);
|
||||
void CollectFileTask();
|
||||
eFileTask m_FileTask{ eFileTask::NONE }; // main thread
|
||||
std::thread m_FileThread;
|
||||
mutable std::mutex m_FileMutex; // guards the members down to m_FileDone
|
||||
std::vector<Removed> m_FileRemoved; // removed since the main thread last looked
|
||||
size_t m_FileChecked{};
|
||||
uint64_t m_FileStoredBytes{}; // EVICT: the total left
|
||||
bool m_FileDone{};
|
||||
DeleteRequest m_Purge; // the purge running (main thread, read by the thread before it starts)
|
||||
std::set<LWOOBJID> m_PurgeSkip; // storage ids being made when it started
|
||||
std::vector<std::pair<LWOOBJID, std::vector<LWOOBJID>>> m_PurgeTargets; // storage id -> rows (empty with `all`)
|
||||
std::map<LWOOBJID, std::vector<LWOOBJID>> m_PurgeRowsOf; // main thread: rows of each target
|
||||
std::vector<LWOOBJID> m_PurgeRows; // main thread: rows whose files are gone, to update
|
||||
size_t m_PurgeRowsDone{};
|
||||
struct PurgeInfo {
|
||||
std::string state; // empty: none ran; running, saving (the rows), done
|
||||
bool modular{};
|
||||
size_t checked{};
|
||||
size_t total{}; // 0 with `all`
|
||||
uint64_t deleted{};
|
||||
uint64_t bytes{};
|
||||
int64_t started{};
|
||||
int64_t finished{};
|
||||
};
|
||||
PurgeInfo m_PurgeInfo; // main thread: PurgeStatus
|
||||
size_t m_EvictedThisTask{}; // main thread
|
||||
|
||||
// Main thread only
|
||||
std::set<std::pair<Kind, LWOOBJID>> m_InFlight; // models, and builds waiting for their combination
|
||||
std::set<LWOOBJID> m_ComboJobs; // combinations being made
|
||||
std::map<LWOOBJID, std::vector<std::pair<LWOOBJID, uint32_t>>> m_ComboRows; // combination -> builds (id, attempts) waiting for it
|
||||
std::map<LWOOBJID, LWOOBJID> m_ComboOf; // build -> combination (StorageId)
|
||||
uint64_t m_Reused{};
|
||||
uint64_t m_Empty{}; // models with no bricks (nothing to make) // builds whose combination was made already
|
||||
std::chrono::steady_clock::time_point m_NextPoll{};
|
||||
std::chrono::steady_clock::time_point m_NextEviction{};
|
||||
std::map<std::pair<Kind, LWOOBJID>, std::pair<std::chrono::steady_clock::time_point, Availability>> m_Recent; // answers for missing files
|
||||
uint64_t m_StoredBytes{};
|
||||
std::vector<LWOOBJID> m_ChangedMeshes; // TakeChangedMeshes
|
||||
uint64_t m_Made{};
|
||||
uint64_t m_Failed{};
|
||||
uint64_t m_Evicted{};
|
||||
struct LogEntry {
|
||||
Kind kind{};
|
||||
LWOOBJID id{};
|
||||
bool ok{};
|
||||
double milliseconds{};
|
||||
std::string message;
|
||||
int64_t time{};
|
||||
};
|
||||
std::deque<LogEntry> m_Log; // the last results
|
||||
|
||||
// Assembled meshes (QueueAssembly) by combination, newest first; workers add to it
|
||||
std::mutex m_AssemblyMutex;
|
||||
std::list<std::pair<std::string, std::shared_ptr<const std::string>>> m_Assemblies;
|
||||
std::shared_ptr<const std::string> CachedAssembly(const std::string& key);
|
||||
void CacheAssembly(const std::string& key, std::shared_ptr<const std::string> nif);
|
||||
};
|
||||
198
dUgcServer/Processing/UgcStorage.cpp
Normal file
198
dUgcServer/Processing/UgcStorage.cpp
Normal file
@@ -0,0 +1,198 @@
|
||||
#include "UgcStorage.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <fstream>
|
||||
#include <random>
|
||||
|
||||
#include "ZCompression.h"
|
||||
|
||||
namespace {
|
||||
constexpr std::array KNOWN_FILES = {
|
||||
"model.nif", "model.nif.gz", "model.nif.checksum", "model.nif.sd0",
|
||||
"model.lxfml.gz", "model.lxfml.checksum",
|
||||
"icon.dds.gz", "icon.dds.checksum", "icon.dds.sd0", "icon.png",
|
||||
"model.noao.nif", "model.noao.nif.gz", "stats.json", "combo.json",
|
||||
"previous.icon.png", "previous.model.nif.gz", "previous.stats.json",
|
||||
};
|
||||
|
||||
// What of the files made before is kept when an item is made again, for comparing (as previous.<name>)
|
||||
constexpr std::array KEPT_FILES = { "icon.png", "model.nif.gz", "stats.json" };
|
||||
|
||||
const char* KindFolder(UgcStorage::Kind kind) {
|
||||
return kind == UgcStorage::Kind::MODEL ? "models" : "modular";
|
||||
}
|
||||
|
||||
std::string RandomSuffix() {
|
||||
static thread_local std::mt19937_64 random{ std::random_device{}() };
|
||||
return std::to_string(random());
|
||||
}
|
||||
}
|
||||
|
||||
UgcStorage::UgcStorage(std::filesystem::path root) : m_Root(std::move(root)) {}
|
||||
|
||||
bool UgcStorage::IsKnownFile(const std::string& name) {
|
||||
return std::find(KNOWN_FILES.begin(), KNOWN_FILES.end(), name) != KNOWN_FILES.end();
|
||||
}
|
||||
|
||||
std::filesystem::path UgcStorage::Folder(Kind kind, LWOOBJID id) const {
|
||||
const auto id64 = static_cast<uint64_t>(id);
|
||||
return m_Root / KindFolder(kind) / std::to_string(id64 % 1000) / std::to_string(id64);
|
||||
}
|
||||
|
||||
std::optional<std::filesystem::path> UgcStorage::File(Kind kind, LWOOBJID id, const std::string& name) const {
|
||||
if (!IsKnownFile(name)) return std::nullopt;
|
||||
auto path = Folder(kind, id) / name;
|
||||
std::error_code error;
|
||||
if (!std::filesystem::is_regular_file(path, error)) return std::nullopt;
|
||||
return path;
|
||||
}
|
||||
|
||||
std::optional<uint64_t> UgcStorage::Write(Kind kind, LWOOBJID id, const Files& files, std::string& error) const {
|
||||
const auto folder = Folder(kind, id);
|
||||
const auto temporary = folder.parent_path() / (".tmp-" + folder.filename().string() + "-" + RandomSuffix());
|
||||
std::error_code code;
|
||||
std::filesystem::create_directories(temporary, code);
|
||||
if (code) {
|
||||
error = "could not create " + temporary.string() + ": " + code.message();
|
||||
return std::nullopt;
|
||||
}
|
||||
uint64_t bytes = 0;
|
||||
for (const auto& [name, data] : files) {
|
||||
std::ofstream out(temporary / name, std::ios::binary | std::ios::trunc);
|
||||
out.write(data.data(), static_cast<std::streamsize>(data.size()));
|
||||
if (!out) {
|
||||
error = "could not write " + (temporary / name).string();
|
||||
std::filesystem::remove_all(temporary, code);
|
||||
return std::nullopt;
|
||||
}
|
||||
bytes += data.size();
|
||||
}
|
||||
// Keep the last version's previews to compare with
|
||||
for (const auto* name : KEPT_FILES) {
|
||||
if (files.contains(name)) {
|
||||
std::filesystem::copy_file(folder / name, temporary / (std::string("previous.") + name), std::filesystem::copy_options::overwrite_existing, code);
|
||||
if (!code) bytes += std::filesystem::file_size(temporary / (std::string("previous.") + name), code);
|
||||
code.clear();
|
||||
}
|
||||
}
|
||||
// Swap the old folder out and the new one in; the old one is deleted after
|
||||
const auto old = folder.parent_path() / (".old-" + folder.filename().string() + "-" + RandomSuffix());
|
||||
const bool hadOld = std::filesystem::exists(folder, code);
|
||||
if (hadOld) std::filesystem::rename(folder, old, code);
|
||||
std::filesystem::rename(temporary, folder, code);
|
||||
if (code) {
|
||||
error = "could not move the files into " + folder.string() + ": " + code.message();
|
||||
std::filesystem::remove_all(temporary, code);
|
||||
return std::nullopt;
|
||||
}
|
||||
if (hadOld) std::filesystem::remove_all(old, code);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
std::optional<uint64_t> UgcStorage::Update(Kind kind, LWOOBJID id, const Files& files, std::string& error) const {
|
||||
const auto folder = Folder(kind, id);
|
||||
std::error_code code;
|
||||
if (!std::filesystem::is_directory(folder, code)) {
|
||||
error = "no files to update in " + folder.string();
|
||||
return std::nullopt;
|
||||
}
|
||||
uint64_t bytes = 0;
|
||||
for (const auto* name : KEPT_FILES) {
|
||||
if (files.contains(name) && std::filesystem::exists(folder / name, code)) {
|
||||
std::filesystem::copy_file(folder / name, folder / (std::string("previous.") + name), std::filesystem::copy_options::overwrite_existing, code);
|
||||
code.clear();
|
||||
}
|
||||
}
|
||||
for (const auto& [name, data] : files) {
|
||||
if (!IsKnownFile(name)) continue;
|
||||
const auto aside = folder / (".tmp-" + name + "-" + RandomSuffix());
|
||||
{
|
||||
std::ofstream out(aside, std::ios::binary | std::ios::trunc);
|
||||
out.write(data.data(), static_cast<std::streamsize>(data.size()));
|
||||
if (!out) {
|
||||
error = "could not write " + aside.string();
|
||||
std::filesystem::remove(aside, code);
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
std::filesystem::rename(aside, folder / name, code);
|
||||
if (code) {
|
||||
error = "could not replace " + (folder / name).string() + ": " + code.message();
|
||||
std::filesystem::remove(aside, code);
|
||||
return std::nullopt;
|
||||
}
|
||||
bytes += data.size();
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
void UgcStorage::Remove(Kind kind, LWOOBJID id) const {
|
||||
std::error_code error;
|
||||
std::filesystem::remove_all(Folder(kind, id), error);
|
||||
}
|
||||
|
||||
void UgcStorage::Touch(Kind kind, LWOOBJID id) const {
|
||||
std::error_code error;
|
||||
std::filesystem::last_write_time(Folder(kind, id), std::filesystem::file_time_type::clock::now(), error);
|
||||
}
|
||||
|
||||
std::vector<UgcStorage::Entry> UgcStorage::List() const {
|
||||
std::vector<Entry> entries;
|
||||
std::error_code error;
|
||||
for (const auto kind : { Kind::MODEL, Kind::MODULAR }) {
|
||||
const auto base = m_Root / KindFolder(kind);
|
||||
for (std::filesystem::directory_iterator bucket(base, error), end; !error && bucket != end; bucket.increment(error)) {
|
||||
if (!bucket->is_directory(error)) continue;
|
||||
for (std::filesystem::directory_iterator item(bucket->path(), error), itemEnd; !error && item != itemEnd; item.increment(error)) {
|
||||
const auto name = item->path().filename().string();
|
||||
if (name.empty() || name[0] == '.' || !item->is_directory(error)) continue;
|
||||
Entry entry;
|
||||
entry.kind = kind;
|
||||
try {
|
||||
entry.id = static_cast<LWOOBJID>(std::stoull(name));
|
||||
} catch (...) {
|
||||
continue;
|
||||
}
|
||||
entry.used = std::filesystem::last_write_time(item->path(), error);
|
||||
for (std::filesystem::directory_iterator file(item->path(), error), fileEnd; !error && file != fileEnd; file.increment(error)) {
|
||||
if (file->is_regular_file(error)) entry.bytes += file->file_size(error);
|
||||
}
|
||||
entries.push_back(entry);
|
||||
}
|
||||
error.clear();
|
||||
}
|
||||
error.clear();
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
std::vector<UgcStorage::Entry> UgcStorage::Evict(uint64_t maxBytes) const {
|
||||
auto entries = List();
|
||||
uint64_t total = 0;
|
||||
for (const auto& entry : entries) total += entry.bytes;
|
||||
std::vector<Entry> removed;
|
||||
if (total <= maxBytes) return removed;
|
||||
std::sort(entries.begin(), entries.end(), [](const Entry& a, const Entry& b) { return a.used < b.used; });
|
||||
for (const auto& entry : entries) {
|
||||
if (total <= maxBytes) break;
|
||||
Remove(entry.kind, entry.id);
|
||||
total -= std::min(total, entry.bytes);
|
||||
removed.push_back(entry);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
std::optional<std::string> UgcStorage::ReadNif(Kind kind, LWOOBJID id, const std::string& name) const {
|
||||
const auto read = [](const std::filesystem::path& path) -> std::optional<std::string> {
|
||||
std::ifstream in(path, std::ios::binary);
|
||||
if (!in) return std::nullopt;
|
||||
return std::string(std::istreambuf_iterator<char>(in), {});
|
||||
};
|
||||
if (const auto packed = File(kind, id, name + ".gz")) {
|
||||
const auto data = read(*packed);
|
||||
return data ? ZCompression::Gunzip(*data) : std::nullopt;
|
||||
}
|
||||
if (const auto plain = File(kind, id, name)) return read(*plain);
|
||||
return std::nullopt;
|
||||
}
|
||||
69
dUgcServer/Processing/UgcStorage.h
Normal file
69
dUgcServer/Processing/UgcStorage.h
Normal file
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "dCommonVars.h"
|
||||
|
||||
/**
|
||||
* Where the UGC server keeps what it makes: <root>/models/<id % 1000>/<id>/ for player models and
|
||||
* <root>/modular/<id % 1000>/<id>/ for modular builds. An item's files are written to a temporary folder and renamed
|
||||
* into place, so a request never sees half of them. Plain file operations, safe from any thread for different items.
|
||||
*/
|
||||
class UgcStorage {
|
||||
public:
|
||||
enum class Kind { MODEL, MODULAR };
|
||||
|
||||
// The files an item has, by name (see FileName)
|
||||
using Files = std::map<std::string, std::string>;
|
||||
|
||||
explicit UgcStorage(std::filesystem::path root);
|
||||
|
||||
const std::filesystem::path& GetRoot() const { return m_Root; }
|
||||
|
||||
std::filesystem::path Folder(Kind kind, LWOOBJID id) const;
|
||||
|
||||
// A file of an item, if it is there; `name` must be one of the names the server writes
|
||||
std::optional<std::filesystem::path> File(Kind kind, LWOOBJID id, const std::string& name) const;
|
||||
|
||||
// Replaces an item's files with `files`; the bytes written, nullopt (and `error`) on failure
|
||||
std::optional<uint64_t> Write(Kind kind, LWOOBJID id, const Files& files, std::string& error) const;
|
||||
|
||||
// Replaces some of an item's files in place (each written aside and renamed over the old one), keeping the others;
|
||||
// the ones kept for comparing (icon.png...) are copied to previous.* first. The bytes written, nullopt when the item
|
||||
// has no folder or a write fails.
|
||||
std::optional<uint64_t> Update(Kind kind, LWOOBJID id, const Files& files, std::string& error) const;
|
||||
|
||||
// Deletes an item's files
|
||||
void Remove(Kind kind, LWOOBJID id) const;
|
||||
|
||||
// Marks an item used now, for eviction
|
||||
void Touch(Kind kind, LWOOBJID id) const;
|
||||
|
||||
struct Entry {
|
||||
Kind kind{};
|
||||
LWOOBJID id{};
|
||||
uint64_t bytes{};
|
||||
std::filesystem::file_time_type used{};
|
||||
};
|
||||
|
||||
// Everything stored, with sizes and when it was last used
|
||||
std::vector<Entry> List() const;
|
||||
|
||||
// Removes the least recently used items until the total is at most `maxBytes`; what was removed
|
||||
std::vector<Entry> Evict(uint64_t maxBytes) const;
|
||||
|
||||
// An item's .nif (`name` model.nif, model.noao.nif or previous.model.nif): stored compressed (<name>.gz), or as it is
|
||||
// (made by older versions); nullopt when there's neither
|
||||
std::optional<std::string> ReadNif(Kind kind, LWOOBJID id, const std::string& name) const;
|
||||
|
||||
// Names the server writes (and serves); anything else is refused
|
||||
static bool IsKnownFile(const std::string& name);
|
||||
|
||||
private:
|
||||
std::filesystem::path m_Root;
|
||||
};
|
||||
120
dUgcServer/Processing/UgcThrottle.cpp
Normal file
120
dUgcServer/Processing/UgcThrottle.cpp
Normal file
@@ -0,0 +1,120 @@
|
||||
#include "UgcThrottle.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
namespace {
|
||||
// Up to this much CPU time may be used ahead of the budget (so short jobs aren't slowed at all)
|
||||
constexpr double BURST_SECONDS = 0.25;
|
||||
// Checkpoints closer together than this only read the clock
|
||||
constexpr double MIN_ACCOUNT_SECONDS = 0.005;
|
||||
|
||||
std::atomic<double> g_Budget{ 0.0 };
|
||||
std::atomic<bool> g_Cancel{ false };
|
||||
std::mutex g_Mutex;
|
||||
double g_Balance = BURST_SECONDS; // CPU seconds that may still be used
|
||||
std::chrono::steady_clock::time_point g_Refilled = std::chrono::steady_clock::now();
|
||||
std::atomic<uint64_t> g_SleptMs{ 0 };
|
||||
std::atomic<int64_t> g_LastSleep{ 0 };
|
||||
|
||||
thread_local double t_LastCpu = -1.0;
|
||||
|
||||
int64_t UnixMs() {
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
|
||||
void Refill(double budget) {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const double elapsed = std::chrono::duration<double>(now - g_Refilled).count();
|
||||
g_Refilled = now;
|
||||
g_Balance = std::min(g_Balance + elapsed * budget, BURST_SECONDS * std::max(budget, 1.0));
|
||||
}
|
||||
}
|
||||
|
||||
namespace UgcThrottle {
|
||||
void SetBudget(double cpus) {
|
||||
std::lock_guard lock(g_Mutex);
|
||||
g_Budget = std::max(cpus, 0.0);
|
||||
g_Balance = std::min(g_Balance, BURST_SECONDS);
|
||||
g_Refilled = std::chrono::steady_clock::now();
|
||||
}
|
||||
|
||||
double GetBudget() {
|
||||
return g_Budget;
|
||||
}
|
||||
|
||||
double ThreadCpuSeconds() {
|
||||
#if defined(CLOCK_THREAD_CPUTIME_ID)
|
||||
timespec ts{};
|
||||
if (clock_gettime(CLOCK_THREAD_CPUTIME_ID, &ts) == 0) return static_cast<double>(ts.tv_sec) + ts.tv_nsec / 1e9;
|
||||
#endif
|
||||
return std::chrono::duration<double>(std::chrono::steady_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
|
||||
void Begin() {
|
||||
t_LastCpu = ThreadCpuSeconds();
|
||||
}
|
||||
|
||||
void Cancel(const bool cancel) { g_Cancel = cancel; }
|
||||
bool IsCancelled() { return g_Cancel; }
|
||||
|
||||
void Checkpoint() {
|
||||
if (g_Cancel) throw Cancelled{};
|
||||
const double budget = g_Budget;
|
||||
if (budget <= 0.0) return;
|
||||
const double cpu = ThreadCpuSeconds();
|
||||
if (t_LastCpu < 0.0) t_LastCpu = cpu;
|
||||
const double used = cpu - t_LastCpu;
|
||||
if (used < MIN_ACCOUNT_SECONDS) return;
|
||||
t_LastCpu = cpu;
|
||||
|
||||
double wait = 0.0;
|
||||
{
|
||||
std::lock_guard lock(g_Mutex);
|
||||
Refill(budget);
|
||||
g_Balance -= used;
|
||||
// Overdrawn: wait until the budget has paid it back. Other threads waiting meanwhile each owe their own
|
||||
// share, so the waits add up to what the budget allows.
|
||||
if (g_Balance < 0.0) wait = -g_Balance / budget;
|
||||
}
|
||||
if (wait <= 0.0) return;
|
||||
wait = std::min(wait, 5.0);
|
||||
g_SleptMs += static_cast<uint64_t>(wait * 1000.0);
|
||||
g_LastSleep = UnixMs();
|
||||
// In short sleeps, so a cancel isn't held up by a long wait
|
||||
const auto until = std::chrono::steady_clock::now() + std::chrono::duration_cast<std::chrono::steady_clock::duration>(std::chrono::duration<double>(wait));
|
||||
while (std::chrono::steady_clock::now() < until) {
|
||||
if (g_Cancel) throw Cancelled{};
|
||||
std::this_thread::sleep_for(std::min<std::chrono::steady_clock::duration>(until - std::chrono::steady_clock::now(), std::chrono::milliseconds(100)));
|
||||
}
|
||||
// Time asleep costs no CPU; don't count this call's own bookkeeping twice
|
||||
t_LastCpu = ThreadCpuSeconds();
|
||||
}
|
||||
|
||||
Stats GetStats() {
|
||||
return { g_SleptMs.load(), g_LastSleep.load() };
|
||||
}
|
||||
|
||||
bool ParseHours(const std::string& text, int& from, int& to) {
|
||||
const auto dash = text.find('-');
|
||||
if (dash == std::string::npos) return false;
|
||||
try {
|
||||
const int a = std::stoi(text.substr(0, dash)), b = std::stoi(text.substr(dash + 1));
|
||||
if (a < 0 || a > 23 || b < 0 || b > 23) return false;
|
||||
from = a;
|
||||
to = b;
|
||||
return true;
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool InHours(int hour, int from, int to) {
|
||||
if (from < 0 || to < 0) return false;
|
||||
return from <= to ? hour >= from && hour <= to : hour >= from || hour <= to;
|
||||
}
|
||||
}
|
||||
43
dUgcServer/Processing/UgcThrottle.h
Normal file
43
dUgcServer/Processing/UgcThrottle.h
Normal file
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
/**
|
||||
* Keeps the UGC workers' CPU use under a budget. The long loops (renders, ambient occlusion, icons) call Checkpoint
|
||||
* every few milliseconds of work; it adds the calling thread's CPU time since its last call to a shared account that
|
||||
* the budget fills at `budget` CPUs per second, and sleeps while the account is overdrawn. So the workers together
|
||||
* average at most the budget however many there are, long jobs included, and without a budget it costs nothing.
|
||||
*/
|
||||
namespace UgcThrottle {
|
||||
// CPUs the workers may use together on average (1.5: one and a half cores); 0 or less: no limit
|
||||
void SetBudget(double cpus);
|
||||
double GetBudget();
|
||||
|
||||
// Account the calling thread's CPU time and sleep when over the budget. Throws Cancelled once Cancel(true) was
|
||||
// called, so a job stops in moments when the server shuts down.
|
||||
void Checkpoint();
|
||||
|
||||
// Thrown by Checkpoint while cancelled: the job is abandoned, not failed (its row stays waiting)
|
||||
struct Cancelled {};
|
||||
// Stop (true) or allow (false) every job's work: set when the server stops, cleared when it starts
|
||||
void Cancel(bool cancel);
|
||||
bool IsCancelled();
|
||||
|
||||
// Start accounting on this thread from now (a worker starting a job), so time spent idle isn't counted
|
||||
void Begin();
|
||||
|
||||
struct Stats {
|
||||
uint64_t sleptMs{}; // since start
|
||||
int64_t lastSleepUnixMs{}; // when it last slept, 0 never
|
||||
};
|
||||
Stats GetStats();
|
||||
|
||||
// This thread's CPU time in seconds
|
||||
double ThreadCpuSeconds();
|
||||
|
||||
// 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
|
||||
bool InHours(int hour, int from, int to);
|
||||
}
|
||||
Reference in New Issue
Block a user