activity_sdk_generation_worker.cpp 51 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193
  1. #include "activity_sdk_generation_worker.h"
  2. #include <Windows.h>
  3. #include <algorithm>
  4. #include <array>
  5. #include <atomic>
  6. #include <cstddef>
  7. #include <cstdio>
  8. #include <cstring>
  9. #include <cwchar>
  10. #include <memory>
  11. #include <new>
  12. #include <span>
  13. #include <string>
  14. #include <utility>
  15. #include <vector>
  16. #include "../../../core/filesystem/path.h"
  17. #include "../../../core/logging/log.h"
  18. #include "../../../core/ui/busy/busy.h"
  19. #include "../../../state/activity_sdk/generated_world/catalog_manifest.h"
  20. #include "../../../state/activity_sdk/generated_world/codec.h"
  21. #include "../../../state/activity_sdk/generated_world/store.h"
  22. #include "../../../state/activity_sdk/generation/internal.h"
  23. #include "../../../state/activity_sdk/identity.h"
  24. #include "../../../state/activity_sdk/runtime.h"
  25. #include "../../../state/build_data/runtime.h"
  26. #include "../../../state/content_manifest/content_manifest_state_runtime.h"
  27. #include "../items/packages/internal.h"
  28. #include "../scenarios/scenario_build.h"
  29. #include "activity_sdk_activity_inventory.h"
  30. #include "activity_sdk_generation_report.h"
  31. #include "activity_sdk_generation_worker_internal.h"
  32. #include "activity_sdk_lua_artifacts.h"
  33. #include "activity_sdk_shard_cleanup.h"
  34. #include "activity_sdk_topology_inventory.h"
  35. #include "scriptable_catalog_builder.h"
  36. namespace sunrise::client::content::activity::sdk_generation {
  37. namespace {
  38. namespace generated = state::activity_sdk::generated_world;
  39. namespace manifest = state::activity_sdk::generated_world::manifest;
  40. namespace shard_store = state::activity_sdk::generated_world::store;
  41. namespace package_reader = middleware::content::packages::reader;
  42. namespace catalog = state::build_data::scriptables;
  43. namespace builder = ::sunrise::client::content::activity::scriptables::internal;
  44. namespace inventory = ::sunrise::client::content::activity::sdk_generation::activity_inventory;
  45. namespace topology = ::sunrise::client::content::activity::sdk_generation::topology_inventory;
  46. namespace lua = ::sunrise::client::content::activity::sdk_generation::lua_artifacts;
  47. namespace worker = worker_internal;
  48. // Generated artifact paths below the scenario root.
  49. constexpr std::wstring_view kSdkDirectorySuffix = L"\\sdk";
  50. constexpr std::wstring_view kScenarioDirectorySuffix = L"\\sdk\\scenarios";
  51. constexpr std::wstring_view kCatalogFileSuffix = L"\\sdk\\catalog.bin";
  52. constexpr std::wstring_view kPackFileSuffix = L"\\activity_sdk.pack";
  53. constexpr std::wstring_view kShardExtension = L".pack";
  54. // Wait this long before probing package data again.
  55. constexpr ULONGLONG kPackageWaitMs = 1'000;
  56. /** A small batch bounds complete snapshots retained while parallel work finishes. */
  57. constexpr std::size_t kScenarioBatchSize = 24;
  58. using worker::binding_ready;
  59. using worker::build_inventory;
  60. using worker::build_scenarios;
  61. using worker::build_world_source;
  62. using worker::convert_binding_completeness;
  63. using worker::find_record;
  64. using worker::kDetailCapacity;
  65. using worker::load_full_record;
  66. using worker::load_record;
  67. using worker::publish_estate;
  68. using worker::publish_progress;
  69. using worker::record_name_matches;
  70. using worker::Scenario;
  71. using worker::ScenarioBuildResult;
  72. using worker::shard_path;
  73. using worker::Work;
  74. SRWLOCK g_lock{SRWLOCK_INIT};
  75. bool g_configured{};
  76. bool g_enabled{};
  77. bool g_luaDeclarations{};
  78. bool g_started{};
  79. bool g_offlineActive{};
  80. ULONGLONG g_packageReadyAfter{};
  81. HANDLE g_thread{};
  82. void* g_module{};
  83. std::wstring g_sdkDirectory{};
  84. std::wstring g_scenarioDirectory{};
  85. std::wstring g_catalogPath{};
  86. std::wstring g_packPath{};
  87. std::atomic_bool g_cancel{};
  88. SRWLOCK g_offlineLock{SRWLOCK_INIT};
  89. OfflineCancelProbe g_offlineCancel{};
  90. void* g_offlineCancelContext{};
  91. OfflineProgressSink g_offlineProgress{};
  92. void* g_offlineProgressContext{};
  93. /** Requires a final catalog identity to be derived from its source and packed row digest. */
  94. [[nodiscard]] bool final_sdk_identity(const generated::Digest& sourceFingerprint,
  95. const manifest::SdkIdentity& sdk) noexcept {
  96. state::activity_sdk::identity::Expected expected{};
  97. return state::activity_sdk::identity::derive(sourceFingerprint, sdk.payloadSha256, expected)
  98. && expected.sdkBuildSha256 == sdk.buildSha256;
  99. }
  100. [[nodiscard]] bool cancelled() noexcept {
  101. if (g_offlineCancel != nullptr && g_offlineCancel(g_offlineCancelContext)) {
  102. return true;
  103. }
  104. return g_cancel.load(std::memory_order_relaxed);
  105. }
  106. /** Emits one timing row for the two user-visible generation stages. */
  107. void log_stage_duration(std::string_view stage,
  108. ULONGLONG started,
  109. std::size_t scenarios,
  110. bool ready) noexcept {
  111. std::array<char, 192> line{};
  112. const int written =
  113. std::snprintf(line.data(),
  114. line.size(),
  115. "ev=activity_sdk_generation stage=%.*s ms=%llu scenarios=%zu result=%s",
  116. static_cast<int>(stage.size()),
  117. stage.data(),
  118. static_cast<unsigned long long>(GetTickCount64() - started),
  119. scenarios,
  120. ready ? "ready" : "failed");
  121. if (written > 0) {
  122. core::log::write(
  123. core::log::Channel::client,
  124. core::log::Level::info,
  125. {line.data(), (std::min)(static_cast<std::size_t>(written), line.size() - 1U)});
  126. }
  127. }
  128. /** Copies the selected installed-content identity while the manifest owns its view. */
  129. [[nodiscard]] bool copy_fingerprint(void* opaque,
  130. const state::content_manifest::View& view) noexcept {
  131. if (opaque == nullptr || view.buildFingerprint.size() != generated::Digest{}.size()) {
  132. return false;
  133. }
  134. auto& output = *static_cast<generated::Digest*>(opaque);
  135. std::copy(view.buildFingerprint.begin(), view.buildFingerprint.end(), output.begin());
  136. return true;
  137. }
  138. /** Accepts an existing directory or creates one exact owned child. */
  139. [[nodiscard]] bool ensure_directory(const std::wstring& path) noexcept {
  140. if (path.empty()) {
  141. return false;
  142. }
  143. if (CreateDirectoryW(path.c_str(), nullptr) != FALSE) {
  144. return true;
  145. }
  146. if (GetLastError() != ERROR_ALREADY_EXISTS) {
  147. return false;
  148. }
  149. const DWORD attributes = GetFileAttributesW(path.c_str());
  150. return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
  151. }
  152. /** Accepts only a missing or empty ordinary output root for the internal offline pass. */
  153. [[nodiscard]] bool isolated_output_ready(std::wstring_view path) noexcept {
  154. if (path.empty()) {
  155. return false;
  156. }
  157. try {
  158. const std::wstring terminated(path);
  159. const DWORD attributes = GetFileAttributesW(terminated.c_str());
  160. if (attributes == INVALID_FILE_ATTRIBUTES) {
  161. const DWORD error = GetLastError();
  162. return error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND;
  163. }
  164. if ((attributes & FILE_ATTRIBUTE_DIRECTORY) == 0
  165. || (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) {
  166. return false;
  167. }
  168. WIN32_FIND_DATAW entry{};
  169. const std::wstring search = terminated + L"\\*";
  170. const HANDLE find = FindFirstFileW(search.c_str(), &entry);
  171. if (find == INVALID_HANDLE_VALUE) {
  172. return GetLastError() == ERROR_FILE_NOT_FOUND;
  173. }
  174. bool empty = true;
  175. do {
  176. const std::wstring_view name(entry.cFileName);
  177. if (name != L"." && name != L"..") {
  178. empty = false;
  179. break;
  180. }
  181. } while (FindNextFileW(find, &entry) != FALSE);
  182. return FindClose(find) != FALSE && empty;
  183. } catch (...) {
  184. return false;
  185. }
  186. }
  187. /** Copies the package-reader inputs into one work item. */
  188. [[nodiscard]] bool prepare_work(Work& work) noexcept {
  189. try {
  190. return state::content_manifest::visit_snapshot(&copy_fingerprint, &work.sourceFingerprint)
  191. && items::packages::collect_keys(work.keys);
  192. } catch (...) {
  193. SecureZeroMemory(&work.keys, sizeof work.keys);
  194. return false;
  195. }
  196. }
  197. /** Adapts the process-wide cancellation flag to the inventory builder callback. */
  198. [[nodiscard]] bool inventory_cancelled(void*) noexcept {
  199. return cancelled();
  200. }
  201. /** Counts the rows behind a refused shard, which the coverage verdict alone cannot name. */
  202. void report_shard_diagnostics(const Scenario& scenario,
  203. const catalog::Snapshot& snapshot) noexcept {
  204. std::size_t incompleteObjects = 0;
  205. std::size_t incompleteSafety = 0;
  206. std::size_t leaves = 0;
  207. std::size_t bareTargets = 0;
  208. for (const catalog::Object& object : snapshot.objects) {
  209. if (!object.complete) {
  210. ++incompleteObjects;
  211. }
  212. if (object.safety == catalog::GroupSafety::incomplete) {
  213. ++incompleteSafety;
  214. }
  215. leaves += object.placedLeafCount;
  216. bareTargets += object.bareTargetCount;
  217. }
  218. std::size_t unresolvedStates = 0;
  219. for (const catalog::State& state : snapshot.states) {
  220. if (!state.resolved) {
  221. ++unresolvedStates;
  222. }
  223. }
  224. const catalog::ContainerPlacementDiagnostics& placements =
  225. snapshot.containerPlacementDiagnostics;
  226. std::array<char, 1024> line{};
  227. const int written =
  228. std::snprintf(line.data(),
  229. line.size(),
  230. "ev=sdk_shard_diag scenario=0x%08X objects=%zu "
  231. "obj_incomplete=%zu obj_safety_incomplete=%zu leaves=%zu "
  232. "bare=%zu states_unresolved=%zu "
  233. "unresolved_reads=%llu spatial_ctx=%u spatial_na=%u spatial_complete=%u "
  234. "spatial_unresolved=%llu spatial_dropped=%llu spatial_semantic=%llu "
  235. "spatial_tables=%zu spatial_owners=%zu spatial_instances=%zu "
  236. "cp_ctx=%u cp_na=%u cp_complete=%u cp_owner_complete=%u "
  237. "cp_unresolved=%llu cp_semantic=%llu cp_dropped=%llu/%llu/%llu/%llu/%llu "
  238. "cp_rows=%zu/%zu/%zu/%zu/%zu",
  239. static_cast<unsigned>(scenario.tag),
  240. snapshot.objects.size(),
  241. incompleteObjects,
  242. incompleteSafety,
  243. leaves,
  244. bareTargets,
  245. unresolvedStates,
  246. static_cast<unsigned long long>(snapshot.unresolvedReads),
  247. snapshot.staticSpatialContextResolved ? 1U : 0U,
  248. snapshot.staticSpatialNotApplicable ? 1U : 0U,
  249. snapshot.staticSpatialComplete ? 1U : 0U,
  250. static_cast<unsigned long long>(snapshot.staticSpatialUnresolvedReads),
  251. static_cast<unsigned long long>(snapshot.staticSpatialDropped),
  252. static_cast<unsigned long long>(snapshot.staticSpatialSemanticUnresolved),
  253. snapshot.staticSpatialTables.size(),
  254. snapshot.staticSpatialOwners.size(),
  255. snapshot.staticSpatialInstances.size(),
  256. placements.contextResolved ? 1U : 0U,
  257. placements.contextNotApplicable ? 1U : 0U,
  258. placements.complete ? 1U : 0U,
  259. placements.identityOwnerInventoryComplete ? 1U : 0U,
  260. static_cast<unsigned long long>(placements.unresolvedReads),
  261. static_cast<unsigned long long>(placements.semanticUnresolved),
  262. static_cast<unsigned long long>(placements.droppedLists),
  263. static_cast<unsigned long long>(placements.droppedOwners),
  264. static_cast<unsigned long long>(placements.droppedPlacements),
  265. static_cast<unsigned long long>(placements.droppedConfigs),
  266. static_cast<unsigned long long>(placements.droppedComponents),
  267. snapshot.containerPlacementLists.size(),
  268. snapshot.containerPlacementOwners.size(),
  269. snapshot.containerPlacements.size(),
  270. snapshot.containerPlacementConfigs.size(),
  271. snapshot.containerPlacementComponents.size());
  272. if (written > 0) {
  273. core::log::write(
  274. core::log::Channel::client,
  275. core::log::Level::error,
  276. {line.data(), (std::min)(static_cast<std::size_t>(written), line.size() - 1)});
  277. }
  278. }
  279. /** Writes one new shard under its deterministic digest name. */
  280. [[nodiscard]] bool build_record(const Work& work,
  281. const package_reader::Source& source,
  282. const builder::ContainerIndex& containers,
  283. package_reader::Scratch& scratch,
  284. builder::ScenarioAnalysisCache& analyses,
  285. const Scenario& scenario,
  286. manifest::Record& record,
  287. std::shared_ptr<const catalog::Snapshot>& output,
  288. std::array<char, kDetailCapacity>& detailScratch,
  289. const char*& detail) {
  290. // Every arm below reported the same word, so a failure named the step it reached and nothing
  291. // about which of five things went wrong. Each one now says which.
  292. const std::string_view name(scenario.name.data(), scenario.nameLength);
  293. const auto snapshot = builder::build_scenario_catalog(
  294. source, containers, scratch, analyses, scenario.tag, name, &cancelled);
  295. if (cancelled()) {
  296. detail = "shard_cancelled";
  297. return false;
  298. }
  299. if (!snapshot) {
  300. detail = "shard_catalog_null";
  301. return false;
  302. }
  303. if (snapshot->status != catalog::BuildStatus::ready) {
  304. const char* stage = "shard_catalog_not_ready";
  305. switch (snapshot->status) {
  306. case catalog::BuildStatus::failed:
  307. stage = "shard_catalog_failed";
  308. break;
  309. case catalog::BuildStatus::idle:
  310. stage = "shard_catalog_idle";
  311. break;
  312. case catalog::BuildStatus::queued:
  313. stage = "shard_catalog_queued";
  314. break;
  315. case catalog::BuildStatus::building:
  316. stage = "shard_catalog_building";
  317. break;
  318. default:
  319. break;
  320. }
  321. report_shard_diagnostics(scenario, *snapshot);
  322. // The builder already names its own refusal. Carry that text out instead of dropping it.
  323. const char* reason = snapshot->detail[0] != '\0' ? snapshot->detail.data() : "no detail";
  324. detailScratch = {};
  325. (void)std::snprintf(detailScratch.data(), detailScratch.size(), "%s:%s", stage, reason);
  326. detail = detailScratch.data();
  327. return false;
  328. }
  329. generated::PreparedShard prepared{};
  330. if (!generated::prepare(work.sourceFingerprint, *snapshot, prepared)) {
  331. detail = "shard_prepare_failed";
  332. return false;
  333. }
  334. const generated::Digest payload = prepared.payload_sha256();
  335. std::wstring finalPath;
  336. generated::Digest written{};
  337. if (!shard_path(work.scenarioDirectory, scenario.tag, payload, finalPath)) {
  338. detail = "shard_path_failed";
  339. return false;
  340. }
  341. if (!generated::publish(finalPath.c_str(), std::move(prepared), written)) {
  342. detail = "shard_write_failed";
  343. return false;
  344. }
  345. record = {};
  346. record.scenarioTag = scenario.tag;
  347. record.scenarioName = scenario.name;
  348. record.scenarioNameLength = scenario.nameLength;
  349. record.shardPayloadSha256 = payload;
  350. output = snapshot;
  351. return true;
  352. }
  353. /** Rewrites one loaded cache hit into an isolated output tree. */
  354. [[nodiscard]] bool
  355. materialize_cached_record(const Work& work,
  356. const Scenario& scenario,
  357. const manifest::Record& record,
  358. const std::shared_ptr<const catalog::Snapshot>& cached,
  359. std::shared_ptr<const catalog::Snapshot>& output) noexcept {
  360. if (cached == nullptr) {
  361. return false;
  362. }
  363. if (work.cacheScenarioDirectory == work.scenarioDirectory) {
  364. output = cached;
  365. return true;
  366. }
  367. std::wstring finalPath;
  368. generated::Digest written{};
  369. if (!shard_path(work.scenarioDirectory, scenario.tag, record.shardPayloadSha256, finalPath)
  370. || !generated::write(finalPath.c_str(), work.sourceFingerprint, *cached, written)) {
  371. return false;
  372. }
  373. output = cached;
  374. return true;
  375. }
  376. /** One indexed result keeps parallel work deterministic when threads finish out of order. */
  377. struct ScenarioBuildResult final {
  378. manifest::Record record{};
  379. std::shared_ptr<const catalog::Snapshot> snapshot{};
  380. std::array<char, kDetailCapacity> detail{};
  381. bool attempted{};
  382. bool ready{};
  383. bool reused{};
  384. };
  385. /** Shared immutable inputs and atomic scheduling state for one scenario batch. */
  386. struct ScenarioBuildBatch final {
  387. const Work* work{};
  388. const package_reader::Source* source{};
  389. const builder::ContainerIndex* containers{};
  390. const manifest::Catalog* prior{};
  391. std::vector<ScenarioBuildResult>* results{};
  392. std::atomic_size_t next{};
  393. std::atomic_size_t completed{};
  394. SRWLOCK progressLock = SRWLOCK_INIT;
  395. std::size_t progressPublished{};
  396. std::size_t firstScenario{};
  397. std::size_t blockCacheSlots{};
  398. std::size_t tableCacheSlots{};
  399. bool priorReady{};
  400. };
  401. /** Caps workers while leaving cores for the game and the server. */
  402. [[nodiscard]] std::size_t scenario_worker_count(std::size_t scenarios) noexcept {
  403. SYSTEM_INFO info{};
  404. GetSystemInfo(&info);
  405. const std::size_t processors = static_cast<std::size_t>(info.dwNumberOfProcessors);
  406. const std::size_t available = processors > 2U ? processors - 2U : 1U;
  407. return (std::min)(scenarios, (std::min)(available, kMaximumScenarioWorkers));
  408. }
  409. /** Divides one cache budget across active workers without leaving a worker uncached. */
  410. [[nodiscard]] std::size_t worker_cache_slots(std::size_t budget, std::size_t workers) noexcept {
  411. return (std::max)(std::size_t{1}, (budget + workers - 1U) / workers);
  412. }
  413. /** Publishes only increasing completion counts from out-of-order workers. */
  414. void publish_parallel_progress(ScenarioBuildBatch& batch, const Scenario& scenario) noexcept {
  415. const std::size_t complete = batch.completed.fetch_add(1U) + 1U;
  416. AcquireSRWLockExclusive(&batch.progressLock);
  417. if (complete <= batch.progressPublished) {
  418. ReleaseSRWLockExclusive(&batch.progressLock);
  419. return;
  420. }
  421. batch.progressPublished = complete;
  422. publish_progress(state::activity_sdk::generation::Status::building,
  423. static_cast<std::uint32_t>(complete),
  424. static_cast<std::uint32_t>(batch.work->scenarios.size()),
  425. scenario.tag,
  426. std::string_view(scenario.name.data(), scenario.nameLength));
  427. ReleaseSRWLockExclusive(&batch.progressLock);
  428. }
  429. /** A cache made before the layout catalogue was ready omitted whole placement domains. */
  430. [[nodiscard]] bool placement_context_ready(const Scenario& scenario,
  431. const catalog::Snapshot& snapshot) noexcept {
  432. state::build_data::scenarios::Definition layout{};
  433. const std::string_view name(scenario.name.data(), scenario.nameLength);
  434. if (!state::build_data::find_scenario_layout(name, layout) || layout.spawnStemLength == 0) {
  435. return true;
  436. }
  437. return snapshot.containerPlacementDiagnostics.contextResolved
  438. && snapshot.staticSpatialContextResolved;
  439. }
  440. /** Builds one worker's contiguous chunks with private package and analysis caches. */
  441. void run_scenario_worker(ScenarioBuildBatch& batch) noexcept {
  442. std::unique_ptr<package_reader::Scratch> scratch(new (std::nothrow) package_reader::Scratch());
  443. if (scratch == nullptr) {
  444. return;
  445. }
  446. if (!package_reader::prepare_blocks(*scratch, batch.blockCacheSlots)) {
  447. (void)package_reader::prepare_blocks(*scratch, 0);
  448. }
  449. if (!package_reader::prepare_tables(*scratch, batch.tableCacheSlots)) {
  450. (void)package_reader::prepare_tables(*scratch, 0);
  451. }
  452. builder::ScenarioAnalysisCache analyses{};
  453. while (!cancelled()) {
  454. const std::size_t begin = batch.next.fetch_add(kScenarioChunkSize);
  455. if (begin >= batch.results->size()) {
  456. break;
  457. }
  458. const std::size_t end = (std::min)(begin + kScenarioChunkSize, batch.results->size());
  459. for (std::size_t index = begin; index < end && !cancelled(); ++index) {
  460. const Scenario& scenario = batch.work->scenarios[batch.firstScenario + index];
  461. ScenarioBuildResult& result = (*batch.results)[index];
  462. result.attempted = true;
  463. try {
  464. const manifest::Record* existing =
  465. batch.priorReady ? find_record(*batch.prior, scenario.tag) : nullptr;
  466. bool kept = existing != nullptr
  467. && load_full_record(batch.work->cacheScenarioDirectory,
  468. batch.work->sourceFingerprint,
  469. scenario,
  470. *existing,
  471. result.snapshot);
  472. if (kept && !placement_context_ready(scenario, *result.snapshot)) {
  473. kept = false;
  474. }
  475. if (kept
  476. && !materialize_cached_record(
  477. *batch.work, scenario, *existing, result.snapshot, result.snapshot)) {
  478. kept = false;
  479. }
  480. if (kept) {
  481. result.record = *existing;
  482. result.reused = true;
  483. result.ready = true;
  484. } else {
  485. const char* detail = "shard_build_failed";
  486. std::array<char, kDetailCapacity> scratchDetail{};
  487. result.ready = build_record(*batch.work,
  488. *batch.source,
  489. *batch.containers,
  490. *scratch,
  491. analyses,
  492. scenario,
  493. result.record,
  494. result.snapshot,
  495. scratchDetail,
  496. detail);
  497. if (!result.ready) {
  498. (void)std::snprintf(
  499. result.detail.data(), result.detail.size(), "%s", detail);
  500. }
  501. }
  502. } catch (...) {
  503. result.ready = false;
  504. (void)std::snprintf(result.detail.data(),
  505. result.detail.size(),
  506. "%s",
  507. "unexpected scenario build exception");
  508. }
  509. publish_parallel_progress(batch, scenario);
  510. }
  511. }
  512. package_reader::close_files(*scratch);
  513. }
  514. /** Adapts one batch worker to the Windows thread ABI. */
  515. DWORD WINAPI scenario_thread_main(void* opaque) noexcept {
  516. run_scenario_worker(*static_cast<ScenarioBuildBatch*>(opaque));
  517. return 0;
  518. }
  519. /** Builds all scenario snapshots in parallel while retaining scenario-order output. */
  520. [[nodiscard]] bool build_scenarios(Work& work,
  521. const package_reader::Source& source,
  522. const builder::ContainerIndex& containers,
  523. const manifest::Catalog& prior,
  524. bool priorReady,
  525. std::size_t firstScenario,
  526. std::size_t scenarioCount,
  527. std::vector<ScenarioBuildResult>& results) {
  528. results.clear();
  529. results.resize(scenarioCount);
  530. ScenarioBuildBatch batch{};
  531. batch.work = &work;
  532. batch.source = &source;
  533. batch.containers = &containers;
  534. batch.prior = &prior;
  535. batch.results = &results;
  536. batch.completed.store(firstScenario);
  537. batch.progressPublished = firstScenario;
  538. batch.firstScenario = firstScenario;
  539. batch.priorReady = priorReady;
  540. const std::size_t workers = scenario_worker_count(results.size());
  541. if (workers == 0) {
  542. return false;
  543. }
  544. batch.blockCacheSlots = worker_cache_slots(kParallelBlockCacheBudget, workers);
  545. batch.tableCacheSlots = worker_cache_slots(kParallelTableCacheBudget, workers);
  546. std::vector<HANDLE> threads;
  547. threads.reserve(workers - 1U);
  548. for (std::size_t index = 1; index < workers; ++index) {
  549. const HANDLE thread = CreateThread(nullptr, 0, &scenario_thread_main, &batch, 0, nullptr);
  550. if (thread != nullptr) {
  551. threads.push_back(thread);
  552. }
  553. }
  554. run_scenario_worker(batch);
  555. for (const HANDLE thread : threads) {
  556. (void)WaitForSingleObject(thread, INFINITE);
  557. (void)CloseHandle(thread);
  558. }
  559. return !cancelled();
  560. }
  561. /** Copies one terminal pass result before worker-owned storage is released. */
  562. void finish_offline(Work& work, bool complete, std::size_t built, std::size_t reused) noexcept {
  563. if (!work.offline || work.offlineStatus == nullptr || work.offlineResult == nullptr) {
  564. return;
  565. }
  566. *work.offlineStatus =
  567. complete ? OfflineBuildStatus::ready
  568. : (cancelled() ? OfflineBuildStatus::cancelled : OfflineBuildStatus::failed);
  569. *work.offlineResult = {
  570. static_cast<std::uint32_t>(work.scenarios.size()),
  571. static_cast<std::uint32_t>(work.activityRoots.size()),
  572. static_cast<std::uint32_t>(work.activityVariants.size()),
  573. static_cast<std::uint32_t>(built),
  574. static_cast<std::uint32_t>(reused),
  575. work.payloadSha256,
  576. work.packBytes,
  577. work.luaFiles,
  578. };
  579. }
  580. /** Owns the full-estate package pass until its catalog publishes or the pass fails. */
  581. DWORD WINAPI thread_main(void* opaque) noexcept {
  582. std::unique_ptr<Work> work(static_cast<Work*>(opaque));
  583. bool complete = false;
  584. std::size_t built = 0;
  585. std::size_t reused = 0;
  586. std::uint32_t failureScenario = 0;
  587. const char* failureDetail = "pass_incomplete";
  588. // Outlives the shard build, because the reported detail can point into it.
  589. std::array<char, kDetailCapacity> detailScratch{};
  590. try {
  591. const package_reader::Source source{work->packageDirectory, &work->keys};
  592. publish_progress(state::activity_sdk::generation::Status::preparing,
  593. 0,
  594. static_cast<std::uint32_t>(
  595. middleware::content::packages::tables::kActivityDefinitionCount),
  596. 0,
  597. "building installed activity inventory");
  598. // The game normally publishes these layouts during boot. An isolated generation
  599. // process has no boot worker, but container and spatial extraction still need them.
  600. bool layoutsReady = state::build_data::scenario_layouts_ready();
  601. if (work->offline && !layoutsReady) {
  602. auto scratch = std::make_unique<package_reader::Scratch>();
  603. constexpr std::size_t kMaximumLayoutSteps = 10'000;
  604. for (std::size_t step = 0; step < kMaximumLayoutSteps && !cancelled() && !layoutsReady;
  605. ++step) {
  606. layoutsReady = scenarios::build(source, *scratch);
  607. }
  608. package_reader::close_files(*scratch);
  609. }
  610. bool inventoried = layoutsReady && build_inventory(*work, source);
  611. if (!inventoried) {
  612. failureDetail = layoutsReady ? "inventory_incomplete" : "scenario_layouts_unavailable";
  613. }
  614. const std::uint32_t total = static_cast<std::uint32_t>(work->scenarios.size());
  615. publish_progress(state::activity_sdk::generation::Status::preparing,
  616. 0,
  617. total,
  618. 0,
  619. "sweeping installed containers once for the whole pass");
  620. // The container sweep does not depend on the scenario. Sweeping it inside the loop read
  621. // the whole install once per scenario and kept one stem's worth of it.
  622. builder::ContainerIndex containers{};
  623. // Shards built against an empty index drop every container placement, so a failed
  624. // sweep must fail the pass instead of silently degrading the output.
  625. const bool containersReady = builder::build_container_index(source, containers, &cancelled);
  626. publish_progress(state::activity_sdk::generation::Status::building,
  627. 0,
  628. total,
  629. 0,
  630. "building generated scenario shards");
  631. manifest::Catalog prior{};
  632. manifest::LoadStatus priorStatus = manifest::LoadStatus::invalid;
  633. const bool priorReady =
  634. inventoried && !work->cacheCatalogPath.empty()
  635. && manifest::load(
  636. work->cacheCatalogPath.c_str(), work->sourceFingerprint, prior, priorStatus);
  637. if (inventoried && !topology::begin(work->activityInventory, work->canonicalTopology)) {
  638. failureDetail = "topology_begin_failed";
  639. }
  640. std::vector<manifest::Record> records;
  641. records.reserve(work->scenarios.size());
  642. work->scenarioWorldSources.clear();
  643. if (work->luaDeclarations) {
  644. work->scenarioWorldSources.reserve(work->scenarios.size());
  645. }
  646. complete = inventoried && containersReady && !work->scenarios.empty()
  647. && work->canonicalTopology.scenarios.size() == work->scenarios.size();
  648. if (inventoried && work->scenarios.empty()) {
  649. failureDetail = "inventory_empty";
  650. } else if (!containersReady) {
  651. failureDetail = cancelled() ? "shard_cancelled" : "container_index_failed";
  652. }
  653. const ULONGLONG buildingStarted = GetTickCount64();
  654. std::vector<ScenarioBuildResult> scenarioResults;
  655. for (std::size_t first = 0; complete && first < work->scenarios.size();
  656. first += kScenarioBatchSize) {
  657. const std::size_t count =
  658. (std::min)(kScenarioBatchSize, work->scenarios.size() - first);
  659. if (!build_scenarios(
  660. *work, source, containers, prior, priorReady, first, count, scenarioResults)) {
  661. complete = false;
  662. failureDetail = cancelled() ? "shard_cancelled" : "scenario_batch_failed";
  663. break;
  664. }
  665. for (std::size_t offset = 0; complete && offset < scenarioResults.size(); ++offset) {
  666. const Scenario& scenario = work->scenarios[first + offset];
  667. ScenarioBuildResult& result = scenarioResults[offset];
  668. if (!result.attempted || !result.ready || result.snapshot == nullptr) {
  669. complete = false;
  670. failureScenario = scenario.tag;
  671. failureDetail =
  672. result.detail[0] != '\0' ? result.detail.data() : "shard_build_failed";
  673. break;
  674. }
  675. built += result.reused ? 0U : 1U;
  676. reused += result.reused ? 1U : 0U;
  677. if (!topology::append(*result.snapshot, work->canonicalTopology)) {
  678. complete = false;
  679. failureScenario = scenario.tag;
  680. failureDetail = "topology_append_failed";
  681. break;
  682. }
  683. if (!external_placements::append(
  684. scenario.tag, *result.snapshot, work->externalPlacements)) {
  685. complete = false;
  686. failureScenario = scenario.tag;
  687. failureDetail = "external_placements_failed";
  688. break;
  689. }
  690. if (work->luaDeclarations) {
  691. lua::ScenarioWorldSource worldSource{};
  692. if (!build_world_source(*result.snapshot, worldSource)) {
  693. complete = false;
  694. failureScenario = scenario.tag;
  695. failureDetail = "lua_world_source_failed";
  696. break;
  697. }
  698. work->scenarioWorldSources.push_back(std::move(worldSource));
  699. }
  700. records.push_back(result.record);
  701. }
  702. }
  703. if (complete && !cancelled() && !topology::finish(work->canonicalTopology)) {
  704. complete = false;
  705. failureDetail = "topology_finish_failed";
  706. }
  707. if (complete && !cancelled()) {
  708. external_placements::finalize(work->externalPlacements);
  709. // A dropped harvest would silently shrink the squad anchors, so it fails the pass.
  710. if (!work->externalPlacements.complete) {
  711. complete = false;
  712. failureDetail = "external_placements_incomplete";
  713. }
  714. }
  715. if (complete && !cancelled()
  716. && !binding_ready(work->activityInventory.bindingCompleteness,
  717. work->activityInventory.activities.size())) {
  718. complete = false;
  719. failureDetail = "binding_incomplete";
  720. }
  721. log_stage_duration("building", buildingStarted, work->scenarios.size(), complete);
  722. if (complete && !cancelled()) {
  723. // The pack stage reports no per-scenario counts, so the overlay bar is indeterminate.
  724. publish_progress(state::activity_sdk::generation::Status::building,
  725. total,
  726. total,
  727. 0,
  728. "building generated SDK pack",
  729. false);
  730. const ULONGLONG packStarted = GetTickCount64();
  731. complete = publish_estate(*work, source, records, failureDetail);
  732. log_stage_duration("pack", packStarted, work->scenarios.size(), complete);
  733. if (complete && !clean_stale_shards(work->scenarioDirectory, records)) {
  734. core::log::write(core::log::Channel::client,
  735. core::log::Level::warn,
  736. "ev=activity_sdk_generation stage=shard_cleanup result=retained");
  737. }
  738. }
  739. if (cancelled()) {
  740. publish_progress(state::activity_sdk::generation::Status::cancelled,
  741. static_cast<std::uint32_t>(records.size()),
  742. static_cast<std::uint32_t>(work->scenarios.size()),
  743. 0,
  744. "generation was cancelled");
  745. } else if (complete) {
  746. publish_progress(state::activity_sdk::generation::Status::ready,
  747. static_cast<std::uint32_t>(records.size()),
  748. static_cast<std::uint32_t>(records.size()),
  749. 0,
  750. "generated SDK pack is ready");
  751. } else {
  752. publish_progress(state::activity_sdk::generation::Status::failed,
  753. static_cast<std::uint32_t>(records.size()),
  754. static_cast<std::uint32_t>(work->scenarios.size()),
  755. failureScenario,
  756. failureDetail);
  757. }
  758. } catch (...) {
  759. publish_progress(state::activity_sdk::generation::Status::failed,
  760. 0,
  761. static_cast<std::uint32_t>(work->scenarios.size()),
  762. 0,
  763. "unexpected generation exception");
  764. complete = false;
  765. }
  766. finish_offline(*work, complete, built, reused);
  767. if (!work->offline) {
  768. report_result({work->scenarios.size(),
  769. work->activityRoots.size(),
  770. work->activityVariants.size(),
  771. built,
  772. reused,
  773. complete ? nullptr : failureDetail,
  774. failureScenario,
  775. complete});
  776. }
  777. SecureZeroMemory(&work->keys, sizeof work->keys);
  778. if (!work->offline) {
  779. core::ui::busy::end(core::ui::busy::Task::sdkGeneration);
  780. }
  781. return 0;
  782. }
  783. } // namespace
  784. /** Adapts the selected live or offline cancellation source to shared stages. */
  785. bool worker_internal::cancel_requested() noexcept {
  786. return cancelled();
  787. }
  788. /** Sends one worker event to the selected live or offline observer. */
  789. void worker_internal::publish_progress(state::activity_sdk::generation::Status status,
  790. std::uint32_t current,
  791. std::uint32_t total,
  792. std::uint32_t scenarioTag,
  793. std::string_view detail,
  794. bool determinate) noexcept {
  795. if (g_offlineProgress != nullptr) {
  796. g_offlineProgress(g_offlineProgressContext, {status, current, total, scenarioTag, detail});
  797. return;
  798. }
  799. core::ui::busy::set_progress(
  800. core::ui::busy::Task::sdkGeneration, current, total, detail, determinate);
  801. state::activity_sdk::generation::internal::publish(status, current, total, scenarioTag, detail);
  802. }
  803. /** Runs one synchronous package pass into an isolated artifact tree. */
  804. OfflineBuildStatus build_offline(const OfflineBuildRequest& request,
  805. OfflineCancelProbe cancel,
  806. void* cancelContext,
  807. OfflineProgressSink progress,
  808. void* progressContext,
  809. OfflineBuildResult& output) noexcept {
  810. output = {};
  811. const bool validFingerprint = std::any_of(request.sourceFingerprint.begin(),
  812. request.sourceFingerprint.end(),
  813. [](std::byte value) { return value != std::byte{}; });
  814. if (request.keys == nullptr) {
  815. return OfflineBuildStatus::invalidInput;
  816. }
  817. const auto keyBytes =
  818. std::span(reinterpret_cast<const std::byte*>(request.keys), sizeof *request.keys);
  819. const bool validKeys = std::any_of(
  820. keyBytes.begin(), keyBytes.end(), [](std::byte value) { return value != std::byte{}; });
  821. if (!validFingerprint || !validKeys || request.packageDirectory.empty()
  822. || request.outputArtifactDirectory.empty()
  823. || !isolated_output_ready(request.outputArtifactDirectory)) {
  824. return OfflineBuildStatus::invalidInput;
  825. }
  826. auto* work = new (std::nothrow) Work();
  827. if (work == nullptr) {
  828. return OfflineBuildStatus::failed;
  829. }
  830. OfflineBuildStatus status = OfflineBuildStatus::failed;
  831. try {
  832. work->keys = *request.keys;
  833. work->sourceFingerprint = request.sourceFingerprint;
  834. work->packageDirectory.assign(request.packageDirectory);
  835. const std::wstring outputRoot(request.outputArtifactDirectory);
  836. const std::wstring cacheRoot(request.cacheArtifactDirectory);
  837. work->sdkDirectory = outputRoot + std::wstring(kSdkDirectorySuffix);
  838. work->scenarioDirectory = outputRoot + std::wstring(kScenarioDirectorySuffix);
  839. work->catalogPath = outputRoot + std::wstring(kCatalogFileSuffix);
  840. work->packPath = outputRoot + std::wstring(kPackFileSuffix);
  841. if (!cacheRoot.empty()) {
  842. work->cacheScenarioDirectory = cacheRoot + std::wstring(kScenarioDirectorySuffix);
  843. work->cacheCatalogPath = cacheRoot + std::wstring(kCatalogFileSuffix);
  844. }
  845. work->offlineResult = &output;
  846. work->offlineStatus = &status;
  847. work->offline = true;
  848. // The isolated export publishes a complete estate, so it always writes the Lua tree.
  849. work->luaDeclarations = true;
  850. } catch (...) {
  851. SecureZeroMemory(&work->keys, sizeof work->keys);
  852. delete work;
  853. return OfflineBuildStatus::invalidInput;
  854. }
  855. const DWORD packageAttributes = GetFileAttributesW(work->packageDirectory.c_str());
  856. if (packageAttributes == INVALID_FILE_ATTRIBUTES
  857. || (packageAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0
  858. || !ensure_directory(std::wstring(request.outputArtifactDirectory))
  859. || !ensure_directory(work->sdkDirectory) || !ensure_directory(work->scenarioDirectory)) {
  860. SecureZeroMemory(&work->keys, sizeof work->keys);
  861. delete work;
  862. return OfflineBuildStatus::invalidInput;
  863. }
  864. AcquireSRWLockExclusive(&g_offlineLock);
  865. AcquireSRWLockExclusive(&g_lock);
  866. const bool busy = g_offlineActive || g_enabled || g_thread != nullptr;
  867. if (!busy) {
  868. g_offlineActive = true;
  869. }
  870. ReleaseSRWLockExclusive(&g_lock);
  871. if (busy) {
  872. ReleaseSRWLockExclusive(&g_offlineLock);
  873. SecureZeroMemory(&work->keys, sizeof work->keys);
  874. delete work;
  875. return OfflineBuildStatus::busy;
  876. }
  877. g_cancel.store(false, std::memory_order_relaxed);
  878. g_offlineCancel = cancel;
  879. g_offlineCancelContext = cancelContext;
  880. g_offlineProgress = progress;
  881. g_offlineProgressContext = progressContext;
  882. (void)thread_main(work);
  883. g_offlineProgressContext = nullptr;
  884. g_offlineProgress = nullptr;
  885. g_offlineCancelContext = nullptr;
  886. g_offlineCancel = nullptr;
  887. g_cancel.store(false, std::memory_order_relaxed);
  888. AcquireSRWLockExclusive(&g_lock);
  889. g_offlineActive = false;
  890. ReleaseSRWLockExclusive(&g_lock);
  891. ReleaseSRWLockExclusive(&g_offlineLock);
  892. return status;
  893. }
  894. /** Stores artifact paths without creating the optional SDK directory. */
  895. void initialize(void* module, const Policy& policy) noexcept {
  896. core::path::Buffer artifact;
  897. const bool resolved = core::path::artifact_directory(module, artifact);
  898. std::wstring sdkDirectory;
  899. std::wstring scenarioDirectory;
  900. std::wstring catalogPath;
  901. std::wstring packPath;
  902. try {
  903. if (resolved) {
  904. sdkDirectory.assign(artifact.chars.data(), artifact.length);
  905. scenarioDirectory = sdkDirectory;
  906. catalogPath = sdkDirectory;
  907. packPath = sdkDirectory;
  908. sdkDirectory.append(kSdkDirectorySuffix);
  909. scenarioDirectory.append(kScenarioDirectorySuffix);
  910. catalogPath.append(kCatalogFileSuffix);
  911. packPath.append(kPackFileSuffix);
  912. }
  913. } catch (...) {
  914. sdkDirectory.clear();
  915. scenarioDirectory.clear();
  916. catalogPath.clear();
  917. packPath.clear();
  918. }
  919. AcquireSRWLockExclusive(&g_lock);
  920. g_configured = resolved && !sdkDirectory.empty() && !scenarioDirectory.empty()
  921. && !catalogPath.empty() && !packPath.empty();
  922. g_enabled = g_configured;
  923. g_luaDeclarations = policy.luaDeclarations;
  924. g_started = false;
  925. g_packageReadyAfter = 0;
  926. g_module = module;
  927. g_sdkDirectory = std::move(sdkDirectory);
  928. g_scenarioDirectory = std::move(scenarioDirectory);
  929. g_catalogPath = std::move(catalogPath);
  930. g_packPath = std::move(packPath);
  931. g_cancel.store(false, std::memory_order_relaxed);
  932. const bool active = g_enabled;
  933. ReleaseSRWLockExclusive(&g_lock);
  934. publish_progress(active ? state::activity_sdk::generation::Status::waiting
  935. : state::activity_sdk::generation::Status::disabled,
  936. 0,
  937. 0,
  938. 0,
  939. active ? "waiting for package data" : "generation is disabled");
  940. }
  941. /** Starts the configured pass once every package-owned input is ready. */
  942. void service() noexcept {
  943. AcquireSRWLockExclusive(&g_lock);
  944. if (g_thread != nullptr && WaitForSingleObject(g_thread, 0) == WAIT_OBJECT_0) {
  945. CloseHandle(g_thread);
  946. g_thread = nullptr;
  947. }
  948. const ULONGLONG now = GetTickCount64();
  949. if (!g_configured || !g_enabled || g_started || g_offlineActive || g_thread != nullptr
  950. || now < g_packageReadyAfter) {
  951. ReleaseSRWLockExclusive(&g_lock);
  952. return;
  953. }
  954. if (state::activity_sdk::status() == state::activity_sdk::Status::ready) {
  955. const state::activity_sdk::Snapshot loaded = state::activity_sdk::snapshot();
  956. const bool current =
  957. loaded && (!g_luaDeclarations || lua::is_current(g_sdkDirectory.c_str(), *loaded));
  958. if (current) {
  959. g_started = true;
  960. publish_progress(state::activity_sdk::generation::Status::ready,
  961. 0,
  962. 0,
  963. 0,
  964. "generated SDK estate is already loaded");
  965. ReleaseSRWLockExclusive(&g_lock);
  966. return;
  967. }
  968. state::activity_sdk::shutdown();
  969. }
  970. auto* work = new (std::nothrow) Work();
  971. if (work == nullptr) {
  972. g_enabled = false;
  973. publish_progress(state::activity_sdk::generation::Status::failed,
  974. 0,
  975. 0,
  976. 0,
  977. "SDK generator allocation failed");
  978. ReleaseSRWLockExclusive(&g_lock);
  979. return;
  980. }
  981. try {
  982. work->module = g_module;
  983. work->luaDeclarations = g_luaDeclarations;
  984. work->sdkDirectory = g_sdkDirectory;
  985. work->scenarioDirectory = g_scenarioDirectory;
  986. work->catalogPath = g_catalogPath;
  987. work->packPath = g_packPath;
  988. } catch (...) {
  989. delete work;
  990. g_enabled = false;
  991. publish_progress(
  992. state::activity_sdk::generation::Status::failed, 0, 0, 0, "SDK generator setup failed");
  993. ReleaseSRWLockExclusive(&g_lock);
  994. return;
  995. }
  996. core::path::Buffer packageDirectory;
  997. const bool ready = work->module != nullptr && !work->sdkDirectory.empty()
  998. && !work->packPath.empty() && state::build_data::scenario_layouts_ready()
  999. && items::packages::package_directory(packageDirectory)
  1000. && prepare_work(*work);
  1001. if (ready) {
  1002. try {
  1003. work->packageDirectory.assign(packageDirectory.chars.data(), packageDirectory.length);
  1004. } catch (...) {
  1005. SecureZeroMemory(&work->keys, sizeof work->keys);
  1006. delete work;
  1007. g_enabled = false;
  1008. publish_progress(state::activity_sdk::generation::Status::failed,
  1009. 0,
  1010. 0,
  1011. 0,
  1012. "SDK package path setup failed");
  1013. ReleaseSRWLockExclusive(&g_lock);
  1014. return;
  1015. }
  1016. }
  1017. if (!ready) {
  1018. SecureZeroMemory(&work->keys, sizeof work->keys);
  1019. delete work;
  1020. publish_progress(
  1021. state::activity_sdk::generation::Status::waiting, 0, 0, 0, "waiting for package data");
  1022. g_packageReadyAfter = now + kPackageWaitMs;
  1023. ReleaseSRWLockExclusive(&g_lock);
  1024. return;
  1025. }
  1026. if (!ensure_directory(work->sdkDirectory) || !ensure_directory(work->scenarioDirectory)) {
  1027. SecureZeroMemory(&work->keys, sizeof work->keys);
  1028. delete work;
  1029. g_enabled = false;
  1030. publish_progress(state::activity_sdk::generation::Status::failed,
  1031. 0,
  1032. 0,
  1033. 0,
  1034. "SDK output directory setup failed");
  1035. ReleaseSRWLockExclusive(&g_lock);
  1036. return;
  1037. }
  1038. g_cancel.store(false, std::memory_order_relaxed);
  1039. publish_progress(
  1040. state::activity_sdk::generation::Status::preparing,
  1041. 0,
  1042. static_cast<std::uint32_t>(middleware::content::packages::tables::kActivityDefinitionCount),
  1043. 0,
  1044. "waiting to build installed activity inventory");
  1045. core::ui::busy::raise(core::ui::busy::Task::sdkGeneration);
  1046. g_thread = CreateThread(nullptr, 0, &thread_main, work, 0, nullptr);
  1047. if (g_thread == nullptr) {
  1048. core::ui::busy::end(core::ui::busy::Task::sdkGeneration);
  1049. SecureZeroMemory(&work->keys, sizeof work->keys);
  1050. delete work;
  1051. publish_progress(state::activity_sdk::generation::Status::failed,
  1052. 0,
  1053. 0,
  1054. 0,
  1055. "SDK generator thread did not start");
  1056. g_enabled = false;
  1057. ReleaseSRWLockExclusive(&g_lock);
  1058. return;
  1059. }
  1060. g_started = true;
  1061. g_packageReadyAfter = 0;
  1062. ReleaseSRWLockExclusive(&g_lock);
  1063. }
  1064. /** Loads only a shard named by the atomically published catalog. */
  1065. bool load_cached_scenario(std::uint32_t scenarioTag,
  1066. std::string_view scenarioName,
  1067. std::shared_ptr<catalog::Snapshot>& output) noexcept {
  1068. output.reset();
  1069. if (scenarioTag == 0 || scenarioName.empty()
  1070. || scenarioName.size() >= catalog::kScenarioNameCapacity) {
  1071. return false;
  1072. }
  1073. std::wstring scenarioDirectory;
  1074. std::wstring catalogPath;
  1075. AcquireSRWLockShared(&g_lock);
  1076. const bool configured = g_configured;
  1077. try {
  1078. scenarioDirectory = g_scenarioDirectory;
  1079. catalogPath = g_catalogPath;
  1080. } catch (...) {
  1081. scenarioDirectory.clear();
  1082. catalogPath.clear();
  1083. }
  1084. ReleaseSRWLockShared(&g_lock);
  1085. if (!configured || scenarioDirectory.empty() || catalogPath.empty()) {
  1086. return false;
  1087. }
  1088. generated::Digest fingerprint{};
  1089. if (!state::content_manifest::visit_snapshot(&copy_fingerprint, &fingerprint)) {
  1090. return false;
  1091. }
  1092. try {
  1093. manifest::Catalog catalog{};
  1094. manifest::LoadStatus manifestStatus = manifest::LoadStatus::invalid;
  1095. if (!manifest::load(catalogPath.c_str(), fingerprint, catalog, manifestStatus)
  1096. || !final_sdk_identity(fingerprint, catalog.sdk)
  1097. || !binding_ready(catalog.bindingCompleteness, catalog.activityVariants.size())) {
  1098. return false;
  1099. }
  1100. const manifest::Record* const record = find_record(catalog, scenarioTag);
  1101. if (record == nullptr || record->scenarioNameLength == 0
  1102. || record->scenarioNameLength >= record->scenarioName.size()
  1103. || !record_name_matches(*record, scenarioTag, scenarioName)) {
  1104. return false;
  1105. }
  1106. Scenario scenario{};
  1107. scenario.tag = scenarioTag;
  1108. scenario.nameLength = record->scenarioNameLength;
  1109. scenario.name = record->scenarioName;
  1110. auto pending = std::make_shared<catalog::Snapshot>();
  1111. if (!load_record(scenarioDirectory, fingerprint, scenario, *record, *pending)) {
  1112. return false;
  1113. }
  1114. output = std::move(pending);
  1115. return true;
  1116. } catch (...) {
  1117. output.reset();
  1118. return false;
  1119. }
  1120. }
  1121. /** Cancels generation before withdrawing all paths and policy. */
  1122. void reset() noexcept {
  1123. HANDLE thread = nullptr;
  1124. AcquireSRWLockExclusive(&g_lock);
  1125. g_enabled = false;
  1126. g_cancel.store(true, std::memory_order_relaxed);
  1127. thread = g_thread;
  1128. ReleaseSRWLockExclusive(&g_lock);
  1129. if (thread != nullptr) {
  1130. WaitForSingleObject(thread, INFINITE);
  1131. }
  1132. AcquireSRWLockExclusive(&g_lock);
  1133. if (g_thread != nullptr) {
  1134. CloseHandle(g_thread);
  1135. g_thread = nullptr;
  1136. }
  1137. g_configured = false;
  1138. g_started = false;
  1139. g_packageReadyAfter = 0;
  1140. g_module = nullptr;
  1141. g_sdkDirectory.clear();
  1142. g_scenarioDirectory.clear();
  1143. g_catalogPath.clear();
  1144. g_packPath.clear();
  1145. ReleaseSRWLockExclusive(&g_lock);
  1146. core::ui::busy::end(core::ui::busy::Task::sdkGeneration);
  1147. state::activity_sdk::generation::internal::clear();
  1148. }
  1149. } // namespace sunrise::client::content::activity::sdk_generation