| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193 |
- #include "activity_sdk_generation_worker.h"
- #include <Windows.h>
- #include <algorithm>
- #include <array>
- #include <atomic>
- #include <cstddef>
- #include <cstdio>
- #include <cstring>
- #include <cwchar>
- #include <memory>
- #include <new>
- #include <span>
- #include <string>
- #include <utility>
- #include <vector>
- #include "../../../core/filesystem/path.h"
- #include "../../../core/logging/log.h"
- #include "../../../core/ui/busy/busy.h"
- #include "../../../state/activity_sdk/generated_world/catalog_manifest.h"
- #include "../../../state/activity_sdk/generated_world/codec.h"
- #include "../../../state/activity_sdk/generated_world/store.h"
- #include "../../../state/activity_sdk/generation/internal.h"
- #include "../../../state/activity_sdk/identity.h"
- #include "../../../state/activity_sdk/runtime.h"
- #include "../../../state/build_data/runtime.h"
- #include "../../../state/content_manifest/content_manifest_state_runtime.h"
- #include "../items/packages/internal.h"
- #include "../scenarios/scenario_build.h"
- #include "activity_sdk_activity_inventory.h"
- #include "activity_sdk_generation_report.h"
- #include "activity_sdk_generation_worker_internal.h"
- #include "activity_sdk_lua_artifacts.h"
- #include "activity_sdk_shard_cleanup.h"
- #include "activity_sdk_topology_inventory.h"
- #include "scriptable_catalog_builder.h"
- namespace sunrise::client::content::activity::sdk_generation {
- namespace {
- namespace generated = state::activity_sdk::generated_world;
- namespace manifest = state::activity_sdk::generated_world::manifest;
- namespace shard_store = state::activity_sdk::generated_world::store;
- namespace package_reader = middleware::content::packages::reader;
- namespace catalog = state::build_data::scriptables;
- namespace builder = ::sunrise::client::content::activity::scriptables::internal;
- namespace inventory = ::sunrise::client::content::activity::sdk_generation::activity_inventory;
- namespace topology = ::sunrise::client::content::activity::sdk_generation::topology_inventory;
- namespace lua = ::sunrise::client::content::activity::sdk_generation::lua_artifacts;
- namespace worker = worker_internal;
- // Generated artifact paths below the scenario root.
- constexpr std::wstring_view kSdkDirectorySuffix = L"\\sdk";
- constexpr std::wstring_view kScenarioDirectorySuffix = L"\\sdk\\scenarios";
- constexpr std::wstring_view kCatalogFileSuffix = L"\\sdk\\catalog.bin";
- constexpr std::wstring_view kPackFileSuffix = L"\\activity_sdk.pack";
- constexpr std::wstring_view kShardExtension = L".pack";
- // Wait this long before probing package data again.
- constexpr ULONGLONG kPackageWaitMs = 1'000;
- /** A small batch bounds complete snapshots retained while parallel work finishes. */
- constexpr std::size_t kScenarioBatchSize = 24;
- using worker::binding_ready;
- using worker::build_inventory;
- using worker::build_scenarios;
- using worker::build_world_source;
- using worker::convert_binding_completeness;
- using worker::find_record;
- using worker::kDetailCapacity;
- using worker::load_full_record;
- using worker::load_record;
- using worker::publish_estate;
- using worker::publish_progress;
- using worker::record_name_matches;
- using worker::Scenario;
- using worker::ScenarioBuildResult;
- using worker::shard_path;
- using worker::Work;
- SRWLOCK g_lock{SRWLOCK_INIT};
- bool g_configured{};
- bool g_enabled{};
- bool g_luaDeclarations{};
- bool g_started{};
- bool g_offlineActive{};
- ULONGLONG g_packageReadyAfter{};
- HANDLE g_thread{};
- void* g_module{};
- std::wstring g_sdkDirectory{};
- std::wstring g_scenarioDirectory{};
- std::wstring g_catalogPath{};
- std::wstring g_packPath{};
- std::atomic_bool g_cancel{};
- SRWLOCK g_offlineLock{SRWLOCK_INIT};
- OfflineCancelProbe g_offlineCancel{};
- void* g_offlineCancelContext{};
- OfflineProgressSink g_offlineProgress{};
- void* g_offlineProgressContext{};
- /** Requires a final catalog identity to be derived from its source and packed row digest. */
- [[nodiscard]] bool final_sdk_identity(const generated::Digest& sourceFingerprint,
- const manifest::SdkIdentity& sdk) noexcept {
- state::activity_sdk::identity::Expected expected{};
- return state::activity_sdk::identity::derive(sourceFingerprint, sdk.payloadSha256, expected)
- && expected.sdkBuildSha256 == sdk.buildSha256;
- }
- [[nodiscard]] bool cancelled() noexcept {
- if (g_offlineCancel != nullptr && g_offlineCancel(g_offlineCancelContext)) {
- return true;
- }
- return g_cancel.load(std::memory_order_relaxed);
- }
- /** Emits one timing row for the two user-visible generation stages. */
- void log_stage_duration(std::string_view stage,
- ULONGLONG started,
- std::size_t scenarios,
- bool ready) noexcept {
- std::array<char, 192> line{};
- const int written =
- std::snprintf(line.data(),
- line.size(),
- "ev=activity_sdk_generation stage=%.*s ms=%llu scenarios=%zu result=%s",
- static_cast<int>(stage.size()),
- stage.data(),
- static_cast<unsigned long long>(GetTickCount64() - started),
- scenarios,
- ready ? "ready" : "failed");
- if (written > 0) {
- core::log::write(
- core::log::Channel::client,
- core::log::Level::info,
- {line.data(), (std::min)(static_cast<std::size_t>(written), line.size() - 1U)});
- }
- }
- /** Copies the selected installed-content identity while the manifest owns its view. */
- [[nodiscard]] bool copy_fingerprint(void* opaque,
- const state::content_manifest::View& view) noexcept {
- if (opaque == nullptr || view.buildFingerprint.size() != generated::Digest{}.size()) {
- return false;
- }
- auto& output = *static_cast<generated::Digest*>(opaque);
- std::copy(view.buildFingerprint.begin(), view.buildFingerprint.end(), output.begin());
- return true;
- }
- /** Accepts an existing directory or creates one exact owned child. */
- [[nodiscard]] bool ensure_directory(const std::wstring& path) noexcept {
- if (path.empty()) {
- return false;
- }
- if (CreateDirectoryW(path.c_str(), nullptr) != FALSE) {
- return true;
- }
- if (GetLastError() != ERROR_ALREADY_EXISTS) {
- return false;
- }
- const DWORD attributes = GetFileAttributesW(path.c_str());
- return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
- }
- /** Accepts only a missing or empty ordinary output root for the internal offline pass. */
- [[nodiscard]] bool isolated_output_ready(std::wstring_view path) noexcept {
- if (path.empty()) {
- return false;
- }
- try {
- const std::wstring terminated(path);
- const DWORD attributes = GetFileAttributesW(terminated.c_str());
- if (attributes == INVALID_FILE_ATTRIBUTES) {
- const DWORD error = GetLastError();
- return error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND;
- }
- if ((attributes & FILE_ATTRIBUTE_DIRECTORY) == 0
- || (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) {
- return false;
- }
- WIN32_FIND_DATAW entry{};
- const std::wstring search = terminated + L"\\*";
- const HANDLE find = FindFirstFileW(search.c_str(), &entry);
- if (find == INVALID_HANDLE_VALUE) {
- return GetLastError() == ERROR_FILE_NOT_FOUND;
- }
- bool empty = true;
- do {
- const std::wstring_view name(entry.cFileName);
- if (name != L"." && name != L"..") {
- empty = false;
- break;
- }
- } while (FindNextFileW(find, &entry) != FALSE);
- return FindClose(find) != FALSE && empty;
- } catch (...) {
- return false;
- }
- }
- /** Copies the package-reader inputs into one work item. */
- [[nodiscard]] bool prepare_work(Work& work) noexcept {
- try {
- return state::content_manifest::visit_snapshot(©_fingerprint, &work.sourceFingerprint)
- && items::packages::collect_keys(work.keys);
- } catch (...) {
- SecureZeroMemory(&work.keys, sizeof work.keys);
- return false;
- }
- }
- /** Adapts the process-wide cancellation flag to the inventory builder callback. */
- [[nodiscard]] bool inventory_cancelled(void*) noexcept {
- return cancelled();
- }
- /** Counts the rows behind a refused shard, which the coverage verdict alone cannot name. */
- void report_shard_diagnostics(const Scenario& scenario,
- const catalog::Snapshot& snapshot) noexcept {
- std::size_t incompleteObjects = 0;
- std::size_t incompleteSafety = 0;
- std::size_t leaves = 0;
- std::size_t bareTargets = 0;
- for (const catalog::Object& object : snapshot.objects) {
- if (!object.complete) {
- ++incompleteObjects;
- }
- if (object.safety == catalog::GroupSafety::incomplete) {
- ++incompleteSafety;
- }
- leaves += object.placedLeafCount;
- bareTargets += object.bareTargetCount;
- }
- std::size_t unresolvedStates = 0;
- for (const catalog::State& state : snapshot.states) {
- if (!state.resolved) {
- ++unresolvedStates;
- }
- }
- const catalog::ContainerPlacementDiagnostics& placements =
- snapshot.containerPlacementDiagnostics;
- std::array<char, 1024> line{};
- const int written =
- std::snprintf(line.data(),
- line.size(),
- "ev=sdk_shard_diag scenario=0x%08X objects=%zu "
- "obj_incomplete=%zu obj_safety_incomplete=%zu leaves=%zu "
- "bare=%zu states_unresolved=%zu "
- "unresolved_reads=%llu spatial_ctx=%u spatial_na=%u spatial_complete=%u "
- "spatial_unresolved=%llu spatial_dropped=%llu spatial_semantic=%llu "
- "spatial_tables=%zu spatial_owners=%zu spatial_instances=%zu "
- "cp_ctx=%u cp_na=%u cp_complete=%u cp_owner_complete=%u "
- "cp_unresolved=%llu cp_semantic=%llu cp_dropped=%llu/%llu/%llu/%llu/%llu "
- "cp_rows=%zu/%zu/%zu/%zu/%zu",
- static_cast<unsigned>(scenario.tag),
- snapshot.objects.size(),
- incompleteObjects,
- incompleteSafety,
- leaves,
- bareTargets,
- unresolvedStates,
- static_cast<unsigned long long>(snapshot.unresolvedReads),
- snapshot.staticSpatialContextResolved ? 1U : 0U,
- snapshot.staticSpatialNotApplicable ? 1U : 0U,
- snapshot.staticSpatialComplete ? 1U : 0U,
- static_cast<unsigned long long>(snapshot.staticSpatialUnresolvedReads),
- static_cast<unsigned long long>(snapshot.staticSpatialDropped),
- static_cast<unsigned long long>(snapshot.staticSpatialSemanticUnresolved),
- snapshot.staticSpatialTables.size(),
- snapshot.staticSpatialOwners.size(),
- snapshot.staticSpatialInstances.size(),
- placements.contextResolved ? 1U : 0U,
- placements.contextNotApplicable ? 1U : 0U,
- placements.complete ? 1U : 0U,
- placements.identityOwnerInventoryComplete ? 1U : 0U,
- static_cast<unsigned long long>(placements.unresolvedReads),
- static_cast<unsigned long long>(placements.semanticUnresolved),
- static_cast<unsigned long long>(placements.droppedLists),
- static_cast<unsigned long long>(placements.droppedOwners),
- static_cast<unsigned long long>(placements.droppedPlacements),
- static_cast<unsigned long long>(placements.droppedConfigs),
- static_cast<unsigned long long>(placements.droppedComponents),
- snapshot.containerPlacementLists.size(),
- snapshot.containerPlacementOwners.size(),
- snapshot.containerPlacements.size(),
- snapshot.containerPlacementConfigs.size(),
- snapshot.containerPlacementComponents.size());
- if (written > 0) {
- core::log::write(
- core::log::Channel::client,
- core::log::Level::error,
- {line.data(), (std::min)(static_cast<std::size_t>(written), line.size() - 1)});
- }
- }
- /** Writes one new shard under its deterministic digest name. */
- [[nodiscard]] bool build_record(const Work& work,
- const package_reader::Source& source,
- const builder::ContainerIndex& containers,
- package_reader::Scratch& scratch,
- builder::ScenarioAnalysisCache& analyses,
- const Scenario& scenario,
- manifest::Record& record,
- std::shared_ptr<const catalog::Snapshot>& output,
- std::array<char, kDetailCapacity>& detailScratch,
- const char*& detail) {
- // Every arm below reported the same word, so a failure named the step it reached and nothing
- // about which of five things went wrong. Each one now says which.
- const std::string_view name(scenario.name.data(), scenario.nameLength);
- const auto snapshot = builder::build_scenario_catalog(
- source, containers, scratch, analyses, scenario.tag, name, &cancelled);
- if (cancelled()) {
- detail = "shard_cancelled";
- return false;
- }
- if (!snapshot) {
- detail = "shard_catalog_null";
- return false;
- }
- if (snapshot->status != catalog::BuildStatus::ready) {
- const char* stage = "shard_catalog_not_ready";
- switch (snapshot->status) {
- case catalog::BuildStatus::failed:
- stage = "shard_catalog_failed";
- break;
- case catalog::BuildStatus::idle:
- stage = "shard_catalog_idle";
- break;
- case catalog::BuildStatus::queued:
- stage = "shard_catalog_queued";
- break;
- case catalog::BuildStatus::building:
- stage = "shard_catalog_building";
- break;
- default:
- break;
- }
- report_shard_diagnostics(scenario, *snapshot);
- // The builder already names its own refusal. Carry that text out instead of dropping it.
- const char* reason = snapshot->detail[0] != '\0' ? snapshot->detail.data() : "no detail";
- detailScratch = {};
- (void)std::snprintf(detailScratch.data(), detailScratch.size(), "%s:%s", stage, reason);
- detail = detailScratch.data();
- return false;
- }
- generated::PreparedShard prepared{};
- if (!generated::prepare(work.sourceFingerprint, *snapshot, prepared)) {
- detail = "shard_prepare_failed";
- return false;
- }
- const generated::Digest payload = prepared.payload_sha256();
- std::wstring finalPath;
- generated::Digest written{};
- if (!shard_path(work.scenarioDirectory, scenario.tag, payload, finalPath)) {
- detail = "shard_path_failed";
- return false;
- }
- if (!generated::publish(finalPath.c_str(), std::move(prepared), written)) {
- detail = "shard_write_failed";
- return false;
- }
- record = {};
- record.scenarioTag = scenario.tag;
- record.scenarioName = scenario.name;
- record.scenarioNameLength = scenario.nameLength;
- record.shardPayloadSha256 = payload;
- output = snapshot;
- return true;
- }
- /** Rewrites one loaded cache hit into an isolated output tree. */
- [[nodiscard]] bool
- materialize_cached_record(const Work& work,
- const Scenario& scenario,
- const manifest::Record& record,
- const std::shared_ptr<const catalog::Snapshot>& cached,
- std::shared_ptr<const catalog::Snapshot>& output) noexcept {
- if (cached == nullptr) {
- return false;
- }
- if (work.cacheScenarioDirectory == work.scenarioDirectory) {
- output = cached;
- return true;
- }
- std::wstring finalPath;
- generated::Digest written{};
- if (!shard_path(work.scenarioDirectory, scenario.tag, record.shardPayloadSha256, finalPath)
- || !generated::write(finalPath.c_str(), work.sourceFingerprint, *cached, written)) {
- return false;
- }
- output = cached;
- return true;
- }
- /** One indexed result keeps parallel work deterministic when threads finish out of order. */
- struct ScenarioBuildResult final {
- manifest::Record record{};
- std::shared_ptr<const catalog::Snapshot> snapshot{};
- std::array<char, kDetailCapacity> detail{};
- bool attempted{};
- bool ready{};
- bool reused{};
- };
- /** Shared immutable inputs and atomic scheduling state for one scenario batch. */
- struct ScenarioBuildBatch final {
- const Work* work{};
- const package_reader::Source* source{};
- const builder::ContainerIndex* containers{};
- const manifest::Catalog* prior{};
- std::vector<ScenarioBuildResult>* results{};
- std::atomic_size_t next{};
- std::atomic_size_t completed{};
- SRWLOCK progressLock = SRWLOCK_INIT;
- std::size_t progressPublished{};
- std::size_t firstScenario{};
- std::size_t blockCacheSlots{};
- std::size_t tableCacheSlots{};
- bool priorReady{};
- };
- /** Caps workers while leaving cores for the game and the server. */
- [[nodiscard]] std::size_t scenario_worker_count(std::size_t scenarios) noexcept {
- SYSTEM_INFO info{};
- GetSystemInfo(&info);
- const std::size_t processors = static_cast<std::size_t>(info.dwNumberOfProcessors);
- const std::size_t available = processors > 2U ? processors - 2U : 1U;
- return (std::min)(scenarios, (std::min)(available, kMaximumScenarioWorkers));
- }
- /** Divides one cache budget across active workers without leaving a worker uncached. */
- [[nodiscard]] std::size_t worker_cache_slots(std::size_t budget, std::size_t workers) noexcept {
- return (std::max)(std::size_t{1}, (budget + workers - 1U) / workers);
- }
- /** Publishes only increasing completion counts from out-of-order workers. */
- void publish_parallel_progress(ScenarioBuildBatch& batch, const Scenario& scenario) noexcept {
- const std::size_t complete = batch.completed.fetch_add(1U) + 1U;
- AcquireSRWLockExclusive(&batch.progressLock);
- if (complete <= batch.progressPublished) {
- ReleaseSRWLockExclusive(&batch.progressLock);
- return;
- }
- batch.progressPublished = complete;
- publish_progress(state::activity_sdk::generation::Status::building,
- static_cast<std::uint32_t>(complete),
- static_cast<std::uint32_t>(batch.work->scenarios.size()),
- scenario.tag,
- std::string_view(scenario.name.data(), scenario.nameLength));
- ReleaseSRWLockExclusive(&batch.progressLock);
- }
- /** A cache made before the layout catalogue was ready omitted whole placement domains. */
- [[nodiscard]] bool placement_context_ready(const Scenario& scenario,
- const catalog::Snapshot& snapshot) noexcept {
- state::build_data::scenarios::Definition layout{};
- const std::string_view name(scenario.name.data(), scenario.nameLength);
- if (!state::build_data::find_scenario_layout(name, layout) || layout.spawnStemLength == 0) {
- return true;
- }
- return snapshot.containerPlacementDiagnostics.contextResolved
- && snapshot.staticSpatialContextResolved;
- }
- /** Builds one worker's contiguous chunks with private package and analysis caches. */
- void run_scenario_worker(ScenarioBuildBatch& batch) noexcept {
- std::unique_ptr<package_reader::Scratch> scratch(new (std::nothrow) package_reader::Scratch());
- if (scratch == nullptr) {
- return;
- }
- if (!package_reader::prepare_blocks(*scratch, batch.blockCacheSlots)) {
- (void)package_reader::prepare_blocks(*scratch, 0);
- }
- if (!package_reader::prepare_tables(*scratch, batch.tableCacheSlots)) {
- (void)package_reader::prepare_tables(*scratch, 0);
- }
- builder::ScenarioAnalysisCache analyses{};
- while (!cancelled()) {
- const std::size_t begin = batch.next.fetch_add(kScenarioChunkSize);
- if (begin >= batch.results->size()) {
- break;
- }
- const std::size_t end = (std::min)(begin + kScenarioChunkSize, batch.results->size());
- for (std::size_t index = begin; index < end && !cancelled(); ++index) {
- const Scenario& scenario = batch.work->scenarios[batch.firstScenario + index];
- ScenarioBuildResult& result = (*batch.results)[index];
- result.attempted = true;
- try {
- const manifest::Record* existing =
- batch.priorReady ? find_record(*batch.prior, scenario.tag) : nullptr;
- bool kept = existing != nullptr
- && load_full_record(batch.work->cacheScenarioDirectory,
- batch.work->sourceFingerprint,
- scenario,
- *existing,
- result.snapshot);
- if (kept && !placement_context_ready(scenario, *result.snapshot)) {
- kept = false;
- }
- if (kept
- && !materialize_cached_record(
- *batch.work, scenario, *existing, result.snapshot, result.snapshot)) {
- kept = false;
- }
- if (kept) {
- result.record = *existing;
- result.reused = true;
- result.ready = true;
- } else {
- const char* detail = "shard_build_failed";
- std::array<char, kDetailCapacity> scratchDetail{};
- result.ready = build_record(*batch.work,
- *batch.source,
- *batch.containers,
- *scratch,
- analyses,
- scenario,
- result.record,
- result.snapshot,
- scratchDetail,
- detail);
- if (!result.ready) {
- (void)std::snprintf(
- result.detail.data(), result.detail.size(), "%s", detail);
- }
- }
- } catch (...) {
- result.ready = false;
- (void)std::snprintf(result.detail.data(),
- result.detail.size(),
- "%s",
- "unexpected scenario build exception");
- }
- publish_parallel_progress(batch, scenario);
- }
- }
- package_reader::close_files(*scratch);
- }
- /** Adapts one batch worker to the Windows thread ABI. */
- DWORD WINAPI scenario_thread_main(void* opaque) noexcept {
- run_scenario_worker(*static_cast<ScenarioBuildBatch*>(opaque));
- return 0;
- }
- /** Builds all scenario snapshots in parallel while retaining scenario-order output. */
- [[nodiscard]] bool build_scenarios(Work& work,
- const package_reader::Source& source,
- const builder::ContainerIndex& containers,
- const manifest::Catalog& prior,
- bool priorReady,
- std::size_t firstScenario,
- std::size_t scenarioCount,
- std::vector<ScenarioBuildResult>& results) {
- results.clear();
- results.resize(scenarioCount);
- ScenarioBuildBatch batch{};
- batch.work = &work;
- batch.source = &source;
- batch.containers = &containers;
- batch.prior = &prior;
- batch.results = &results;
- batch.completed.store(firstScenario);
- batch.progressPublished = firstScenario;
- batch.firstScenario = firstScenario;
- batch.priorReady = priorReady;
- const std::size_t workers = scenario_worker_count(results.size());
- if (workers == 0) {
- return false;
- }
- batch.blockCacheSlots = worker_cache_slots(kParallelBlockCacheBudget, workers);
- batch.tableCacheSlots = worker_cache_slots(kParallelTableCacheBudget, workers);
- std::vector<HANDLE> threads;
- threads.reserve(workers - 1U);
- for (std::size_t index = 1; index < workers; ++index) {
- const HANDLE thread = CreateThread(nullptr, 0, &scenario_thread_main, &batch, 0, nullptr);
- if (thread != nullptr) {
- threads.push_back(thread);
- }
- }
- run_scenario_worker(batch);
- for (const HANDLE thread : threads) {
- (void)WaitForSingleObject(thread, INFINITE);
- (void)CloseHandle(thread);
- }
- return !cancelled();
- }
- /** Copies one terminal pass result before worker-owned storage is released. */
- void finish_offline(Work& work, bool complete, std::size_t built, std::size_t reused) noexcept {
- if (!work.offline || work.offlineStatus == nullptr || work.offlineResult == nullptr) {
- return;
- }
- *work.offlineStatus =
- complete ? OfflineBuildStatus::ready
- : (cancelled() ? OfflineBuildStatus::cancelled : OfflineBuildStatus::failed);
- *work.offlineResult = {
- static_cast<std::uint32_t>(work.scenarios.size()),
- static_cast<std::uint32_t>(work.activityRoots.size()),
- static_cast<std::uint32_t>(work.activityVariants.size()),
- static_cast<std::uint32_t>(built),
- static_cast<std::uint32_t>(reused),
- work.payloadSha256,
- work.packBytes,
- work.luaFiles,
- };
- }
- /** Owns the full-estate package pass until its catalog publishes or the pass fails. */
- DWORD WINAPI thread_main(void* opaque) noexcept {
- std::unique_ptr<Work> work(static_cast<Work*>(opaque));
- bool complete = false;
- std::size_t built = 0;
- std::size_t reused = 0;
- std::uint32_t failureScenario = 0;
- const char* failureDetail = "pass_incomplete";
- // Outlives the shard build, because the reported detail can point into it.
- std::array<char, kDetailCapacity> detailScratch{};
- try {
- const package_reader::Source source{work->packageDirectory, &work->keys};
- publish_progress(state::activity_sdk::generation::Status::preparing,
- 0,
- static_cast<std::uint32_t>(
- middleware::content::packages::tables::kActivityDefinitionCount),
- 0,
- "building installed activity inventory");
- // The game normally publishes these layouts during boot. An isolated generation
- // process has no boot worker, but container and spatial extraction still need them.
- bool layoutsReady = state::build_data::scenario_layouts_ready();
- if (work->offline && !layoutsReady) {
- auto scratch = std::make_unique<package_reader::Scratch>();
- constexpr std::size_t kMaximumLayoutSteps = 10'000;
- for (std::size_t step = 0; step < kMaximumLayoutSteps && !cancelled() && !layoutsReady;
- ++step) {
- layoutsReady = scenarios::build(source, *scratch);
- }
- package_reader::close_files(*scratch);
- }
- bool inventoried = layoutsReady && build_inventory(*work, source);
- if (!inventoried) {
- failureDetail = layoutsReady ? "inventory_incomplete" : "scenario_layouts_unavailable";
- }
- const std::uint32_t total = static_cast<std::uint32_t>(work->scenarios.size());
- publish_progress(state::activity_sdk::generation::Status::preparing,
- 0,
- total,
- 0,
- "sweeping installed containers once for the whole pass");
- // The container sweep does not depend on the scenario. Sweeping it inside the loop read
- // the whole install once per scenario and kept one stem's worth of it.
- builder::ContainerIndex containers{};
- // Shards built against an empty index drop every container placement, so a failed
- // sweep must fail the pass instead of silently degrading the output.
- const bool containersReady = builder::build_container_index(source, containers, &cancelled);
- publish_progress(state::activity_sdk::generation::Status::building,
- 0,
- total,
- 0,
- "building generated scenario shards");
- manifest::Catalog prior{};
- manifest::LoadStatus priorStatus = manifest::LoadStatus::invalid;
- const bool priorReady =
- inventoried && !work->cacheCatalogPath.empty()
- && manifest::load(
- work->cacheCatalogPath.c_str(), work->sourceFingerprint, prior, priorStatus);
- if (inventoried && !topology::begin(work->activityInventory, work->canonicalTopology)) {
- failureDetail = "topology_begin_failed";
- }
- std::vector<manifest::Record> records;
- records.reserve(work->scenarios.size());
- work->scenarioWorldSources.clear();
- if (work->luaDeclarations) {
- work->scenarioWorldSources.reserve(work->scenarios.size());
- }
- complete = inventoried && containersReady && !work->scenarios.empty()
- && work->canonicalTopology.scenarios.size() == work->scenarios.size();
- if (inventoried && work->scenarios.empty()) {
- failureDetail = "inventory_empty";
- } else if (!containersReady) {
- failureDetail = cancelled() ? "shard_cancelled" : "container_index_failed";
- }
- const ULONGLONG buildingStarted = GetTickCount64();
- std::vector<ScenarioBuildResult> scenarioResults;
- for (std::size_t first = 0; complete && first < work->scenarios.size();
- first += kScenarioBatchSize) {
- const std::size_t count =
- (std::min)(kScenarioBatchSize, work->scenarios.size() - first);
- if (!build_scenarios(
- *work, source, containers, prior, priorReady, first, count, scenarioResults)) {
- complete = false;
- failureDetail = cancelled() ? "shard_cancelled" : "scenario_batch_failed";
- break;
- }
- for (std::size_t offset = 0; complete && offset < scenarioResults.size(); ++offset) {
- const Scenario& scenario = work->scenarios[first + offset];
- ScenarioBuildResult& result = scenarioResults[offset];
- if (!result.attempted || !result.ready || result.snapshot == nullptr) {
- complete = false;
- failureScenario = scenario.tag;
- failureDetail =
- result.detail[0] != '\0' ? result.detail.data() : "shard_build_failed";
- break;
- }
- built += result.reused ? 0U : 1U;
- reused += result.reused ? 1U : 0U;
- if (!topology::append(*result.snapshot, work->canonicalTopology)) {
- complete = false;
- failureScenario = scenario.tag;
- failureDetail = "topology_append_failed";
- break;
- }
- if (!external_placements::append(
- scenario.tag, *result.snapshot, work->externalPlacements)) {
- complete = false;
- failureScenario = scenario.tag;
- failureDetail = "external_placements_failed";
- break;
- }
- if (work->luaDeclarations) {
- lua::ScenarioWorldSource worldSource{};
- if (!build_world_source(*result.snapshot, worldSource)) {
- complete = false;
- failureScenario = scenario.tag;
- failureDetail = "lua_world_source_failed";
- break;
- }
- work->scenarioWorldSources.push_back(std::move(worldSource));
- }
- records.push_back(result.record);
- }
- }
- if (complete && !cancelled() && !topology::finish(work->canonicalTopology)) {
- complete = false;
- failureDetail = "topology_finish_failed";
- }
- if (complete && !cancelled()) {
- external_placements::finalize(work->externalPlacements);
- // A dropped harvest would silently shrink the squad anchors, so it fails the pass.
- if (!work->externalPlacements.complete) {
- complete = false;
- failureDetail = "external_placements_incomplete";
- }
- }
- if (complete && !cancelled()
- && !binding_ready(work->activityInventory.bindingCompleteness,
- work->activityInventory.activities.size())) {
- complete = false;
- failureDetail = "binding_incomplete";
- }
- log_stage_duration("building", buildingStarted, work->scenarios.size(), complete);
- if (complete && !cancelled()) {
- // The pack stage reports no per-scenario counts, so the overlay bar is indeterminate.
- publish_progress(state::activity_sdk::generation::Status::building,
- total,
- total,
- 0,
- "building generated SDK pack",
- false);
- const ULONGLONG packStarted = GetTickCount64();
- complete = publish_estate(*work, source, records, failureDetail);
- log_stage_duration("pack", packStarted, work->scenarios.size(), complete);
- if (complete && !clean_stale_shards(work->scenarioDirectory, records)) {
- core::log::write(core::log::Channel::client,
- core::log::Level::warn,
- "ev=activity_sdk_generation stage=shard_cleanup result=retained");
- }
- }
- if (cancelled()) {
- publish_progress(state::activity_sdk::generation::Status::cancelled,
- static_cast<std::uint32_t>(records.size()),
- static_cast<std::uint32_t>(work->scenarios.size()),
- 0,
- "generation was cancelled");
- } else if (complete) {
- publish_progress(state::activity_sdk::generation::Status::ready,
- static_cast<std::uint32_t>(records.size()),
- static_cast<std::uint32_t>(records.size()),
- 0,
- "generated SDK pack is ready");
- } else {
- publish_progress(state::activity_sdk::generation::Status::failed,
- static_cast<std::uint32_t>(records.size()),
- static_cast<std::uint32_t>(work->scenarios.size()),
- failureScenario,
- failureDetail);
- }
- } catch (...) {
- publish_progress(state::activity_sdk::generation::Status::failed,
- 0,
- static_cast<std::uint32_t>(work->scenarios.size()),
- 0,
- "unexpected generation exception");
- complete = false;
- }
- finish_offline(*work, complete, built, reused);
- if (!work->offline) {
- report_result({work->scenarios.size(),
- work->activityRoots.size(),
- work->activityVariants.size(),
- built,
- reused,
- complete ? nullptr : failureDetail,
- failureScenario,
- complete});
- }
- SecureZeroMemory(&work->keys, sizeof work->keys);
- if (!work->offline) {
- core::ui::busy::end(core::ui::busy::Task::sdkGeneration);
- }
- return 0;
- }
- } // namespace
- /** Adapts the selected live or offline cancellation source to shared stages. */
- bool worker_internal::cancel_requested() noexcept {
- return cancelled();
- }
- /** Sends one worker event to the selected live or offline observer. */
- void worker_internal::publish_progress(state::activity_sdk::generation::Status status,
- std::uint32_t current,
- std::uint32_t total,
- std::uint32_t scenarioTag,
- std::string_view detail,
- bool determinate) noexcept {
- if (g_offlineProgress != nullptr) {
- g_offlineProgress(g_offlineProgressContext, {status, current, total, scenarioTag, detail});
- return;
- }
- core::ui::busy::set_progress(
- core::ui::busy::Task::sdkGeneration, current, total, detail, determinate);
- state::activity_sdk::generation::internal::publish(status, current, total, scenarioTag, detail);
- }
- /** Runs one synchronous package pass into an isolated artifact tree. */
- OfflineBuildStatus build_offline(const OfflineBuildRequest& request,
- OfflineCancelProbe cancel,
- void* cancelContext,
- OfflineProgressSink progress,
- void* progressContext,
- OfflineBuildResult& output) noexcept {
- output = {};
- const bool validFingerprint = std::any_of(request.sourceFingerprint.begin(),
- request.sourceFingerprint.end(),
- [](std::byte value) { return value != std::byte{}; });
- if (request.keys == nullptr) {
- return OfflineBuildStatus::invalidInput;
- }
- const auto keyBytes =
- std::span(reinterpret_cast<const std::byte*>(request.keys), sizeof *request.keys);
- const bool validKeys = std::any_of(
- keyBytes.begin(), keyBytes.end(), [](std::byte value) { return value != std::byte{}; });
- if (!validFingerprint || !validKeys || request.packageDirectory.empty()
- || request.outputArtifactDirectory.empty()
- || !isolated_output_ready(request.outputArtifactDirectory)) {
- return OfflineBuildStatus::invalidInput;
- }
- auto* work = new (std::nothrow) Work();
- if (work == nullptr) {
- return OfflineBuildStatus::failed;
- }
- OfflineBuildStatus status = OfflineBuildStatus::failed;
- try {
- work->keys = *request.keys;
- work->sourceFingerprint = request.sourceFingerprint;
- work->packageDirectory.assign(request.packageDirectory);
- const std::wstring outputRoot(request.outputArtifactDirectory);
- const std::wstring cacheRoot(request.cacheArtifactDirectory);
- work->sdkDirectory = outputRoot + std::wstring(kSdkDirectorySuffix);
- work->scenarioDirectory = outputRoot + std::wstring(kScenarioDirectorySuffix);
- work->catalogPath = outputRoot + std::wstring(kCatalogFileSuffix);
- work->packPath = outputRoot + std::wstring(kPackFileSuffix);
- if (!cacheRoot.empty()) {
- work->cacheScenarioDirectory = cacheRoot + std::wstring(kScenarioDirectorySuffix);
- work->cacheCatalogPath = cacheRoot + std::wstring(kCatalogFileSuffix);
- }
- work->offlineResult = &output;
- work->offlineStatus = &status;
- work->offline = true;
- // The isolated export publishes a complete estate, so it always writes the Lua tree.
- work->luaDeclarations = true;
- } catch (...) {
- SecureZeroMemory(&work->keys, sizeof work->keys);
- delete work;
- return OfflineBuildStatus::invalidInput;
- }
- const DWORD packageAttributes = GetFileAttributesW(work->packageDirectory.c_str());
- if (packageAttributes == INVALID_FILE_ATTRIBUTES
- || (packageAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0
- || !ensure_directory(std::wstring(request.outputArtifactDirectory))
- || !ensure_directory(work->sdkDirectory) || !ensure_directory(work->scenarioDirectory)) {
- SecureZeroMemory(&work->keys, sizeof work->keys);
- delete work;
- return OfflineBuildStatus::invalidInput;
- }
- AcquireSRWLockExclusive(&g_offlineLock);
- AcquireSRWLockExclusive(&g_lock);
- const bool busy = g_offlineActive || g_enabled || g_thread != nullptr;
- if (!busy) {
- g_offlineActive = true;
- }
- ReleaseSRWLockExclusive(&g_lock);
- if (busy) {
- ReleaseSRWLockExclusive(&g_offlineLock);
- SecureZeroMemory(&work->keys, sizeof work->keys);
- delete work;
- return OfflineBuildStatus::busy;
- }
- g_cancel.store(false, std::memory_order_relaxed);
- g_offlineCancel = cancel;
- g_offlineCancelContext = cancelContext;
- g_offlineProgress = progress;
- g_offlineProgressContext = progressContext;
- (void)thread_main(work);
- g_offlineProgressContext = nullptr;
- g_offlineProgress = nullptr;
- g_offlineCancelContext = nullptr;
- g_offlineCancel = nullptr;
- g_cancel.store(false, std::memory_order_relaxed);
- AcquireSRWLockExclusive(&g_lock);
- g_offlineActive = false;
- ReleaseSRWLockExclusive(&g_lock);
- ReleaseSRWLockExclusive(&g_offlineLock);
- return status;
- }
- /** Stores artifact paths without creating the optional SDK directory. */
- void initialize(void* module, const Policy& policy) noexcept {
- core::path::Buffer artifact;
- const bool resolved = core::path::artifact_directory(module, artifact);
- std::wstring sdkDirectory;
- std::wstring scenarioDirectory;
- std::wstring catalogPath;
- std::wstring packPath;
- try {
- if (resolved) {
- sdkDirectory.assign(artifact.chars.data(), artifact.length);
- scenarioDirectory = sdkDirectory;
- catalogPath = sdkDirectory;
- packPath = sdkDirectory;
- sdkDirectory.append(kSdkDirectorySuffix);
- scenarioDirectory.append(kScenarioDirectorySuffix);
- catalogPath.append(kCatalogFileSuffix);
- packPath.append(kPackFileSuffix);
- }
- } catch (...) {
- sdkDirectory.clear();
- scenarioDirectory.clear();
- catalogPath.clear();
- packPath.clear();
- }
- AcquireSRWLockExclusive(&g_lock);
- g_configured = resolved && !sdkDirectory.empty() && !scenarioDirectory.empty()
- && !catalogPath.empty() && !packPath.empty();
- g_enabled = g_configured;
- g_luaDeclarations = policy.luaDeclarations;
- g_started = false;
- g_packageReadyAfter = 0;
- g_module = module;
- g_sdkDirectory = std::move(sdkDirectory);
- g_scenarioDirectory = std::move(scenarioDirectory);
- g_catalogPath = std::move(catalogPath);
- g_packPath = std::move(packPath);
- g_cancel.store(false, std::memory_order_relaxed);
- const bool active = g_enabled;
- ReleaseSRWLockExclusive(&g_lock);
- publish_progress(active ? state::activity_sdk::generation::Status::waiting
- : state::activity_sdk::generation::Status::disabled,
- 0,
- 0,
- 0,
- active ? "waiting for package data" : "generation is disabled");
- }
- /** Starts the configured pass once every package-owned input is ready. */
- void service() noexcept {
- AcquireSRWLockExclusive(&g_lock);
- if (g_thread != nullptr && WaitForSingleObject(g_thread, 0) == WAIT_OBJECT_0) {
- CloseHandle(g_thread);
- g_thread = nullptr;
- }
- const ULONGLONG now = GetTickCount64();
- if (!g_configured || !g_enabled || g_started || g_offlineActive || g_thread != nullptr
- || now < g_packageReadyAfter) {
- ReleaseSRWLockExclusive(&g_lock);
- return;
- }
- if (state::activity_sdk::status() == state::activity_sdk::Status::ready) {
- const state::activity_sdk::Snapshot loaded = state::activity_sdk::snapshot();
- const bool current =
- loaded && (!g_luaDeclarations || lua::is_current(g_sdkDirectory.c_str(), *loaded));
- if (current) {
- g_started = true;
- publish_progress(state::activity_sdk::generation::Status::ready,
- 0,
- 0,
- 0,
- "generated SDK estate is already loaded");
- ReleaseSRWLockExclusive(&g_lock);
- return;
- }
- state::activity_sdk::shutdown();
- }
- auto* work = new (std::nothrow) Work();
- if (work == nullptr) {
- g_enabled = false;
- publish_progress(state::activity_sdk::generation::Status::failed,
- 0,
- 0,
- 0,
- "SDK generator allocation failed");
- ReleaseSRWLockExclusive(&g_lock);
- return;
- }
- try {
- work->module = g_module;
- work->luaDeclarations = g_luaDeclarations;
- work->sdkDirectory = g_sdkDirectory;
- work->scenarioDirectory = g_scenarioDirectory;
- work->catalogPath = g_catalogPath;
- work->packPath = g_packPath;
- } catch (...) {
- delete work;
- g_enabled = false;
- publish_progress(
- state::activity_sdk::generation::Status::failed, 0, 0, 0, "SDK generator setup failed");
- ReleaseSRWLockExclusive(&g_lock);
- return;
- }
- core::path::Buffer packageDirectory;
- const bool ready = work->module != nullptr && !work->sdkDirectory.empty()
- && !work->packPath.empty() && state::build_data::scenario_layouts_ready()
- && items::packages::package_directory(packageDirectory)
- && prepare_work(*work);
- if (ready) {
- try {
- work->packageDirectory.assign(packageDirectory.chars.data(), packageDirectory.length);
- } catch (...) {
- SecureZeroMemory(&work->keys, sizeof work->keys);
- delete work;
- g_enabled = false;
- publish_progress(state::activity_sdk::generation::Status::failed,
- 0,
- 0,
- 0,
- "SDK package path setup failed");
- ReleaseSRWLockExclusive(&g_lock);
- return;
- }
- }
- if (!ready) {
- SecureZeroMemory(&work->keys, sizeof work->keys);
- delete work;
- publish_progress(
- state::activity_sdk::generation::Status::waiting, 0, 0, 0, "waiting for package data");
- g_packageReadyAfter = now + kPackageWaitMs;
- ReleaseSRWLockExclusive(&g_lock);
- return;
- }
- if (!ensure_directory(work->sdkDirectory) || !ensure_directory(work->scenarioDirectory)) {
- SecureZeroMemory(&work->keys, sizeof work->keys);
- delete work;
- g_enabled = false;
- publish_progress(state::activity_sdk::generation::Status::failed,
- 0,
- 0,
- 0,
- "SDK output directory setup failed");
- ReleaseSRWLockExclusive(&g_lock);
- return;
- }
- g_cancel.store(false, std::memory_order_relaxed);
- publish_progress(
- state::activity_sdk::generation::Status::preparing,
- 0,
- static_cast<std::uint32_t>(middleware::content::packages::tables::kActivityDefinitionCount),
- 0,
- "waiting to build installed activity inventory");
- core::ui::busy::raise(core::ui::busy::Task::sdkGeneration);
- g_thread = CreateThread(nullptr, 0, &thread_main, work, 0, nullptr);
- if (g_thread == nullptr) {
- core::ui::busy::end(core::ui::busy::Task::sdkGeneration);
- SecureZeroMemory(&work->keys, sizeof work->keys);
- delete work;
- publish_progress(state::activity_sdk::generation::Status::failed,
- 0,
- 0,
- 0,
- "SDK generator thread did not start");
- g_enabled = false;
- ReleaseSRWLockExclusive(&g_lock);
- return;
- }
- g_started = true;
- g_packageReadyAfter = 0;
- ReleaseSRWLockExclusive(&g_lock);
- }
- /** Loads only a shard named by the atomically published catalog. */
- bool load_cached_scenario(std::uint32_t scenarioTag,
- std::string_view scenarioName,
- std::shared_ptr<catalog::Snapshot>& output) noexcept {
- output.reset();
- if (scenarioTag == 0 || scenarioName.empty()
- || scenarioName.size() >= catalog::kScenarioNameCapacity) {
- return false;
- }
- std::wstring scenarioDirectory;
- std::wstring catalogPath;
- AcquireSRWLockShared(&g_lock);
- const bool configured = g_configured;
- try {
- scenarioDirectory = g_scenarioDirectory;
- catalogPath = g_catalogPath;
- } catch (...) {
- scenarioDirectory.clear();
- catalogPath.clear();
- }
- ReleaseSRWLockShared(&g_lock);
- if (!configured || scenarioDirectory.empty() || catalogPath.empty()) {
- return false;
- }
- generated::Digest fingerprint{};
- if (!state::content_manifest::visit_snapshot(©_fingerprint, &fingerprint)) {
- return false;
- }
- try {
- manifest::Catalog catalog{};
- manifest::LoadStatus manifestStatus = manifest::LoadStatus::invalid;
- if (!manifest::load(catalogPath.c_str(), fingerprint, catalog, manifestStatus)
- || !final_sdk_identity(fingerprint, catalog.sdk)
- || !binding_ready(catalog.bindingCompleteness, catalog.activityVariants.size())) {
- return false;
- }
- const manifest::Record* const record = find_record(catalog, scenarioTag);
- if (record == nullptr || record->scenarioNameLength == 0
- || record->scenarioNameLength >= record->scenarioName.size()
- || !record_name_matches(*record, scenarioTag, scenarioName)) {
- return false;
- }
- Scenario scenario{};
- scenario.tag = scenarioTag;
- scenario.nameLength = record->scenarioNameLength;
- scenario.name = record->scenarioName;
- auto pending = std::make_shared<catalog::Snapshot>();
- if (!load_record(scenarioDirectory, fingerprint, scenario, *record, *pending)) {
- return false;
- }
- output = std::move(pending);
- return true;
- } catch (...) {
- output.reset();
- return false;
- }
- }
- /** Cancels generation before withdrawing all paths and policy. */
- void reset() noexcept {
- HANDLE thread = nullptr;
- AcquireSRWLockExclusive(&g_lock);
- g_enabled = false;
- g_cancel.store(true, std::memory_order_relaxed);
- thread = g_thread;
- ReleaseSRWLockExclusive(&g_lock);
- if (thread != nullptr) {
- WaitForSingleObject(thread, INFINITE);
- }
- AcquireSRWLockExclusive(&g_lock);
- if (g_thread != nullptr) {
- CloseHandle(g_thread);
- g_thread = nullptr;
- }
- g_configured = false;
- g_started = false;
- g_packageReadyAfter = 0;
- g_module = nullptr;
- g_sdkDirectory.clear();
- g_scenarioDirectory.clear();
- g_catalogPath.clear();
- g_packPath.clear();
- ReleaseSRWLockExclusive(&g_lock);
- core::ui::busy::end(core::ui::busy::Task::sdkGeneration);
- state::activity_sdk::generation::internal::clear();
- }
- } // namespace sunrise::client::content::activity::sdk_generation
|