Skip to content

Commit f226782

Browse files
authored
feat: Initialize renderer in generated services script (#292)
1 parent ba03f59 commit f226782

File tree

7 files changed

+228
-66
lines changed

7 files changed

+228
-66
lines changed

editor/src/tasks/TaskManager.js

+23-11
Original file line numberDiff line numberDiff line change
@@ -48,22 +48,32 @@ import {Task} from "./task/Task.js";
4848
*/
4949

5050
/**
51-
* This contains all expected configs based on the task type. This is used for
51+
* This contains all expected tasks based on the task type. This is used for
5252
* better autocompletions for `runTask` or `runChildTask`. However, since it's possible
5353
* for plugins to add their own task types, if an unknown task type is used, the expected
5454
* type of the config parameter of the function is 'unknown'.
5555
* @typedef {{
56-
* "renda:buildApplication": import("./task/TaskBuildApplication.js").TaskBuildApplicationConfig,
57-
* "renda:bundleAssets": import("./task/TaskBundleAssets.js").TaskBundleAssetsConfig,
58-
* "renda:bundleScripts": import("./task/TaskBundleScripts.js").TaskBundleScriptsConfig,
59-
* "renda:generateHtml": import("./task/TaskGenerateHtml.js").TaskGenerateHtmlConfig,
60-
* "renda:generateServices": import("./task/TaskGenerateServices.js").TaskGenerateServicesConfig,
61-
* }} KnownTaskConfigs
56+
* "renda:buildApplication": import("./task/TaskBuildApplication.js").TaskBuildApplication,
57+
* "renda:bundleAssets": import("./task/TaskBundleAssets.js").TaskBundleAssets,
58+
* "renda:bundleScripts": import("./task/TaskBundleScripts.js").TaskBundleScripts,
59+
* "renda:generateHtml": import("./task/TaskGenerateHtml.js").TaskGenerateHtml,
60+
* "renda:generateServices": import("./task/TaskGenerateServices.js").TaskGenerateServices,
61+
* }} KnownTasks
6262
*/
6363

6464
/**
6565
* @template {string} T
66-
* @typedef {T extends keyof KnownTaskConfigs ? KnownTaskConfigs[T] : unknown} GetExpectedTaskConfig
66+
* @typedef {T extends keyof KnownTasks ? KnownTasks[T] : unknown} GetExpectedTask
67+
*/
68+
69+
/**
70+
* @template {string} T
71+
* @typedef {GetExpectedTask<T> extends import("./task/Task.js").Task<infer TConfig> ? TConfig : unknown} GetExpectedTaskConfig
72+
*/
73+
74+
/**
75+
* @template {string} T
76+
* @typedef {GetExpectedTask<T> extends import("./task/Task.js").Task<any, infer TCustomData> ? import("./task/Task.js").RunTaskReturn<TCustomData> : never} GetExpectedTaskReturn
6777
*/
6878

6979
export class TaskManager {
@@ -164,14 +174,15 @@ export class TaskManager {
164174
* @param {RunTaskOptions} [options]
165175
*/
166176
async runTask(taskType, taskConfig, options) {
167-
return await this.#runTaskInternal(taskType, taskConfig, options);
177+
const result = await this.#runTaskInternal(taskType, taskConfig, options);
178+
return /** @type {GetExpectedTaskReturn<T>} */ (result);
168179
}
169180

170181
/**
171182
* @param {string} taskType
172183
* @param {unknown} taskConfig
173184
* @param {RunTaskOptions} options
174-
* @returns {Promise<import("./task/Task.js").RunTaskReturn>}
185+
* @returns {Promise<import("./task/Task.js").RunTaskReturn<any>>}
175186
*/
176187
async #runTaskInternal(taskType, taskConfig, options = {}) {
177188
const task = this.initializeTask(taskType);
@@ -224,14 +235,15 @@ export class TaskManager {
224235
});
225236
},
226237
runChildTask: async (taskType, taskConfig, options = {}) => {
227-
return await this.#runTaskInternal(taskType, taskConfig, {
238+
const result = await this.#runTaskInternal(taskType, taskConfig, {
228239
...options,
229240
allowDiskWrites: options.allowDiskWrites ?? allowDiskWrites,
230241
environmentVariables: {
231242
...environmentVariables,
232243
...options.environmentVariables,
233244
},
234245
});
246+
return /** @type {GetExpectedTaskReturn<typeof taskType>} */ (result);
235247
},
236248
});
237249

editor/src/tasks/task/Task.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44

55
/**
6+
* @template [TCustomData = unknown]
67
* @typedef RunTaskReturn
78
* @property {RunTaskCreateAssetData[]} [writeAssets] A list of assets that this task should create when done running.
89
* This is useful if you want to modify files in a very basic way. The assets will be created and written to the output location.
@@ -15,6 +16,7 @@
1516
* flag is set to true.
1617
* @property {import("../../../../src/mod.js").UuidString[]} [touchedAssets] A list of assets that this task touched, or
1718
* might touch when the task is run a second time. This is used by other tasks for determining if this task needs to run before them.
19+
* @property {TCustomData} [customData] Custom data that a task might return, this is only useful when a task is run from a script.
1820
*/
1921

2022
/**
@@ -52,6 +54,7 @@
5254

5355
/**
5456
* @template [TTaskConfig = unknown]
57+
* @template [TCustomData = unknown]
5558
*/
5659
export class Task {
5760
/**
@@ -125,7 +128,7 @@ export class Task {
125128

126129
/**
127130
* @param {RunTaskOptions<TTaskConfig>} options
128-
* @returns {Promise<RunTaskReturn>}
131+
* @returns {Promise<RunTaskReturn<TCustomData>>}
129132
*/
130133
async runTask(options) {
131134
throw new Error(`Task "${this.constructor.name}" does not implement runTask().`);

editor/src/tasks/task/TaskBuildApplication.js

+13-5
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export class TaskBuildApplication extends Task {
6262
}
6363

6464
/**
65+
* Asserts if a task resulted in writing exactly one asset.
6566
* @param {import("./Task.js").RunTaskReturn} taskResult
6667
*/
6768
#assertSingleWriteAsset(taskResult) {
@@ -72,6 +73,7 @@ export class TaskBuildApplication extends Task {
7273
}
7374

7475
/**
76+
* Asserts if a task resulted in writing exactly one asset that is a string.
7577
* @param {import("./Task.js").RunTaskReturn} taskResult
7678
*/
7779
#assertSingleStringWriteAsset(taskResult) {
@@ -129,21 +131,27 @@ export class TaskBuildApplication extends Task {
129131
return result;
130132
},
131133
/**
132-
* @param {number} contextId
133-
* @param {import("../../../../src/mod.js").UuidString[]} uuids
134+
* @param {Object} options
135+
* @param {number} options.contextId
136+
* @param {import("../../../../src/mod.js").UuidString[]} options.usedAssetsUuids
137+
* @param {import("../../../../src/mod.js").UuidString[]} options.entryPointUuids
134138
*/
135-
generateServices: async (contextId, uuids) => {
139+
generateServices: async ({contextId, usedAssetsUuids, entryPointUuids}) => {
136140
const context = this.getContext(contextId);
137141
const generateServicesResult = await context.runChildTask("renda:generateServices", {
138142
outputLocation: ["services.js"],
139-
usedAssets: uuids,
143+
usedAssets: usedAssetsUuids,
144+
entryPoints: entryPointUuids,
140145
}, {
141146
allowDiskWrites: false,
142147
});
143148
const servicesScript = this.#assertSingleStringWriteAsset(generateServicesResult);
144149

145150
return {
146-
returnValue: servicesScript,
151+
returnValue: {
152+
servicesScript,
153+
usedAssets: generateServicesResult.customData?.usedAssets || [],
154+
},
147155
};
148156
},
149157
/**

editor/src/tasks/task/TaskGenerateServices.js

+109-32
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import {CLUSTER_BOUNDS_SHADER_ASSET_UUID, CLUSTER_LIGHTS_SHADER_ASSET_UUID} from "../../../../src/mod.js";
2+
import {ProjectAssetTypeJavascript} from "../../assets/projectAssetType/ProjectAssetTypeJavascript.js";
13
import {createTreeViewStructure} from "../../ui/propertiesTreeView/createStructureHelpers.js";
24
import {Task} from "./Task.js";
35

@@ -17,6 +19,10 @@ import {Task} from "./Task.js";
1719
* asset uuids that are expected to be loaded. The task uses this list to determine
1820
* which asset loader types should be included. This way, loader types that are
1921
* not needed are not included when bundling with tree shaking enabled.
22+
* @property {import("../../../../src/mod.js").UuidString[]} entryPoints A list of
23+
* JavaScript asset uuids that will be using the generated services file. It is used to
24+
* analyse which services are actually used so that any unused services can be omitted
25+
* from the generation.
2026
*/
2127

2228
/**
@@ -56,12 +62,27 @@ import {Task} from "./Task.js";
5662
* to the current ProjectAssetType config.
5763
*/
5864

59-
/** @extends {Task<TaskGenerateServicesConfig>} */
65+
/**
66+
* @typedef TaskGenerateServicesCustomData
67+
* @property {import("../../../../src/mod.js").UuidString[]} usedAssets
68+
*/
69+
70+
/** @extends {Task<TaskGenerateServicesConfig, TaskGenerateServicesCustomData>} */
6071
export class TaskGenerateServices extends Task {
6172
static uiName = "Generate Services";
6273
static type = "renda:generateServices";
6374

6475
static configStructure = createTreeViewStructure({
76+
entryPoints: {
77+
type: "array",
78+
tooltip: "A list of script files that is checked for usage of the generated services. Some parts of the generated services script are omitted depending on usage, in order to reduce bundle size.",
79+
guiOpts: {
80+
arrayType: "droppable",
81+
arrayGuiOpts: {
82+
supportedAssetTypes: [ProjectAssetTypeJavascript],
83+
},
84+
},
85+
},
6586
outputLocation: {
6687
type: "array",
6788
guiOpts: {
@@ -70,13 +91,15 @@ export class TaskGenerateServices extends Task {
7091
},
7192
usedAssets: {
7293
type: "array",
94+
tooltip: "A list of assets that are expected to be loaded. Unused asset types are stripped from the asset loader in order to reduce bundle size.",
7395
guiOpts: {
7496
arrayType: "droppable",
7597
},
7698
},
7799
});
78100

79101
/**
102+
* @override
80103
* @param {import("./Task.js").RunTaskOptions<TaskGenerateServicesConfig>} options
81104
*/
82105
async runTask({config}) {
@@ -89,20 +112,27 @@ export class TaskGenerateServices extends Task {
89112
throw new Error("Failed to run task: no asset manager.");
90113
}
91114

92-
/** @type {Map<import("../../assets/projectAssetType/ProjectAssetType.js").ProjectAssetTypeIdentifier, Set<import("../../assets/ProjectAsset.js").ProjectAssetAny>>} */
93-
const assetTypes = new Map();
94-
for await (const uuid of assetManager.collectAllAssetReferences(config.usedAssets)) {
95-
const asset = await assetManager.getProjectAssetFromUuid(uuid);
96-
if (asset?.assetType) {
97-
let assetsSet = assetTypes.get(asset.assetType);
98-
if (!assetsSet) {
99-
assetsSet = new Set();
100-
assetTypes.set(asset.assetType, assetsSet);
101-
}
102-
assetsSet.add(asset);
115+
let entryPointContents = "";
116+
for (const entryPoint of config.entryPoints) {
117+
const asset = await assetManager.getProjectAssetFromUuid(entryPoint, {
118+
assertAssetType: ProjectAssetTypeJavascript,
119+
});
120+
if (asset) {
121+
const content = await asset.readAssetData();
122+
entryPointContents += content;
103123
}
104124
}
105125

126+
// TODO: This is a pretty primitive way of figuring out which services are needed, since we are
127+
// essentially just looking if some strings of text appear in one of the entry points.
128+
// Ideally we would parse the ast and figure out what is being used that way.
129+
// It would add a lot of complexity and wouldn't be able to handle all cases, but right now
130+
// if large files are used as entry points, the chance of these strings existing a pretty big so
131+
// there could be a lot of false positives.
132+
let needsAssetLoader = entryPointContents.includes("assetLoader");
133+
let needsEngineAssetsManager = entryPointContents.includes("engineAssetsManager");
134+
const needsRenderer = entryPointContents.includes("renderer");
135+
106136
/** @type {Map<string, Set<string>>} */
107137
const collectedImports = new Map();
108138
/**
@@ -118,6 +148,14 @@ export class TaskGenerateServices extends Task {
118148
identifiers.add(identifier);
119149
}
120150

151+
/**
152+
* A list of asset uuids that the engine requires to function.
153+
* Assets in this list will automatically get bundled when the services
154+
* script is generated as part of the 'build application' task.
155+
* @type {Set<import("../../../../src/mod.js").UuidString>}
156+
*/
157+
const usedAssets = new Set();
158+
121159
/**
122160
* @typedef CollectedAssetLoaderType
123161
* @property {string} instanceIdentifier
@@ -131,30 +169,52 @@ export class TaskGenerateServices extends Task {
131169
/** @type {Set<string>} */
132170
const collectedExportIdentifiers = new Set();
133171

134-
for (const [assetTypeIdentifier, assets] of assetTypes) {
135-
const assetType = this.editorInstance.projectAssetTypeManager.getAssetType(assetTypeIdentifier);
136-
const config = assetType?.assetLoaderTypeImportConfig;
137-
if (config) {
138-
const moduleSpecifier = config.moduleSpecifier || "renda";
139-
addImport(config.identifier, moduleSpecifier);
140-
let extra = "";
141-
if (config.extra) {
142-
extra = await config.extra({
143-
editor: this.editorInstance,
144-
addImport,
145-
usedAssets: Array.from(assets),
172+
if (needsAssetLoader) {
173+
/** @type {Map<import("../../assets/projectAssetType/ProjectAssetType.js").ProjectAssetTypeIdentifier, Set<import("../../assets/ProjectAsset.js").ProjectAssetAny>>} */
174+
const assetTypes = new Map();
175+
for await (const uuid of assetManager.collectAllAssetReferences(config.usedAssets)) {
176+
const asset = await assetManager.getProjectAssetFromUuid(uuid);
177+
if (asset?.assetType) {
178+
let assetsSet = assetTypes.get(asset.assetType);
179+
if (!assetsSet) {
180+
assetsSet = new Set();
181+
assetTypes.set(asset.assetType, assetsSet);
182+
}
183+
assetsSet.add(asset);
184+
}
185+
}
186+
187+
for (const [assetTypeIdentifier, assets] of assetTypes) {
188+
const assetType = this.editorInstance.projectAssetTypeManager.getAssetType(assetTypeIdentifier);
189+
const config = assetType?.assetLoaderTypeImportConfig;
190+
if (config) {
191+
const moduleSpecifier = config.moduleSpecifier || "renda";
192+
addImport(config.identifier, moduleSpecifier);
193+
let extra = "";
194+
if (config.extra) {
195+
extra = await config.extra({
196+
editor: this.editorInstance,
197+
addImport,
198+
usedAssets: Array.from(assets),
199+
});
200+
}
201+
collectedAssetLoaderTypes.set(config.identifier, {
202+
instanceIdentifier: config.instanceIdentifier || "",
203+
extra,
204+
returnInstanceIdentifier: config.returnInstanceIdentifier ?? true,
146205
});
147206
}
148-
collectedAssetLoaderTypes.set(config.identifier, {
149-
instanceIdentifier: config.instanceIdentifier || "",
150-
extra,
151-
returnInstanceIdentifier: config.returnInstanceIdentifier ?? true,
152-
});
153207
}
154208
}
155209

156-
const needsAssetLoader = collectedAssetLoaderTypes.size > 0;
157-
210+
if (needsRenderer) {
211+
addImport("WebGpuRenderer", "renda");
212+
needsEngineAssetsManager = true;
213+
}
214+
if (needsEngineAssetsManager) {
215+
addImport("EngineAssetsManager", "renda");
216+
needsAssetLoader = true;
217+
}
158218
if (needsAssetLoader) {
159219
addImport("AssetLoader", "renda");
160220
}
@@ -169,6 +229,7 @@ export class TaskGenerateServices extends Task {
169229

170230
// Services instantiation
171231
code += "export function initializeServices() {\n";
232+
172233
if (needsAssetLoader) {
173234
code += " const assetLoader = new AssetLoader();\n";
174235
code += "\n";
@@ -193,12 +254,25 @@ export class TaskGenerateServices extends Task {
193254
}
194255
}
195256

257+
if (needsEngineAssetsManager) {
258+
code += "const engineAssetsManager = new EngineAssetsManager(assetLoader);";
259+
collectedExportIdentifiers.add("engineAssetsManager");
260+
}
261+
262+
if (needsRenderer) {
263+
code += "const renderer = new WebGpuRenderer(engineAssetsManager);";
264+
code += "renderer.init();";
265+
collectedExportIdentifiers.add("renderer");
266+
usedAssets.add(CLUSTER_BOUNDS_SHADER_ASSET_UUID);
267+
usedAssets.add(CLUSTER_LIGHTS_SHADER_ASSET_UUID);
268+
}
269+
196270
// Return object
197271
code += ` return {${Array.from(collectedExportIdentifiers).join(", ")}};\n`;
198272

199273
code += "}\n";
200274

201-
/** @type {import("./Task.js").RunTaskReturn} */
275+
/** @type {import("./Task.js").RunTaskReturn<TaskGenerateServicesCustomData>} */
202276
const result = {
203277
writeAssets: [
204278
{
@@ -207,6 +281,9 @@ export class TaskGenerateServices extends Task {
207281
fileData: code,
208282
},
209283
],
284+
customData: {
285+
usedAssets: Array.from(usedAssets),
286+
},
210287
};
211288
return result;
212289
}

0 commit comments

Comments
 (0)