Skip to content

Unload scene specific resources on demand #7381

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Core/GDCore/Project/Layout.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ Layout::Layout()
backgroundColorG(209),
backgroundColorB(209),
stopSoundsOnStartup(true),
shouldUnloadAssetsWhenUnloaded(false),
standardSortMethod(true),
disableInputWhenNotFocused(true),
variables(gd::VariablesContainer::SourceType::Scene),
Expand Down Expand Up @@ -244,6 +245,7 @@ void Layout::SerializeTo(SerializerElement& element) const {
element.SetAttribute("title", GetWindowDefaultTitle());
element.SetAttribute("standardSortMethod", standardSortMethod);
element.SetAttribute("stopSoundsOnStartup", stopSoundsOnStartup);
element.SetAttribute("shouldUnloadAssetsWhenUnloaded", shouldUnloadAssetsWhenUnloaded);
element.SetAttribute("disableInputWhenNotFocused",
disableInputWhenNotFocused);

Expand Down Expand Up @@ -304,6 +306,7 @@ void Layout::UnserializeFrom(gd::Project& project,
element.GetStringAttribute("title", "(No title)", "titre"));
standardSortMethod = element.GetBoolAttribute("standardSortMethod");
stopSoundsOnStartup = element.GetBoolAttribute("stopSoundsOnStartup");
shouldUnloadAssetsWhenUnloaded = element.GetBoolAttribute("shouldUnloadAssetsWhenUnloaded");
disableInputWhenNotFocused =
element.GetBoolAttribute("disableInputWhenNotFocused");

Expand Down Expand Up @@ -391,6 +394,7 @@ void Layout::Init(const Layout& other) {
standardSortMethod = other.standardSortMethod;
title = other.title;
stopSoundsOnStartup = other.stopSoundsOnStartup;
shouldUnloadAssetsWhenUnloaded = other.shouldUnloadAssetsWhenUnloaded;
disableInputWhenNotFocused = other.disableInputWhenNotFocused;
initialInstances = other.initialInstances;
layers = other.layers;
Expand Down
13 changes: 13 additions & 0 deletions Core/GDCore/Project/Layout.h
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,18 @@ class GD_CORE_API Layout {
* launched
*/
bool StopSoundsOnStartup() const { return stopSoundsOnStartup; }

/**
* Set if the scene must unload all assets after exit scene
*/
void SetShouldUnloadAssetsWhenUnloaded(bool enable = false) {
shouldUnloadAssetsWhenUnloaded = enable;
}

/**
* Return true if the scene must unload all assets after the exit scene
*/
bool ShouldUnloadAssetsWhenUnloaded() const { return shouldUnloadAssetsWhenUnloaded; }
///@}

/** \name Saving and loading
Expand Down Expand Up @@ -381,6 +393,7 @@ class GD_CORE_API Layout {
behaviorsSharedData; ///< Initial shared datas of behaviors
bool stopSoundsOnStartup = true; ///< True to make the scene stop all sounds at
///< startup.
bool shouldUnloadAssetsWhenUnloaded = false; ///< True to unload scene assets after exit scene
bool standardSortMethod = true; ///< True to sort objects using standard sort.
bool disableInputWhenNotFocused = true; /// If set to true, the input must be
/// disabled when the window do not have the
Expand Down
21 changes: 21 additions & 0 deletions Extensions/Spine/managers/pixi-spine-atlas-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,5 +201,26 @@ namespace gdjs {
this._loadedSpineAtlases.clear();
this._loadingSpineAtlases.clear();
}

/**
* To be called when the scene is disposed.
* Clear the Spine Atlases loaded in this manager.
* @param resourcesList The list of specific resources
*/
disposeByResourcesList(resourcesList: ResourceData[]): void {
resourcesList.forEach((resourceData) => {
const loadedSpineAtlas = this._loadedSpineAtlases.get(resourceData);
if (loadedSpineAtlas) {
loadedSpineAtlas.dispose();
this._loadedSpineAtlases.delete(resourceData);
}

const loadingSpineAtlas = this._loadingSpineAtlases.get(resourceData);
if (loadingSpineAtlas) {
loadingSpineAtlas.then((atl) => atl.dispose());
this._loadingSpineAtlases.delete(resourceData);
}
});
}
}
}
14 changes: 14 additions & 0 deletions Extensions/Spine/managers/pixi-spine-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,5 +126,19 @@ namespace gdjs {
dispose(): void {
this._loadedSpines.clear();
}

/**
* To be called when the scene is disposed.
* Clear the Spine skeleton data loaded in this manager.
* @param resourcesList The list of specific resources
*/
disposeByResourcesList(resourcesList: ResourceData[]): void {
resourcesList.forEach((resourceData) => {
const loadedSpine = this._loadedSpines.get(resourceData);
if (loadedSpine) {
this._loadedSpines.delete(resourceData);
}
});
}
}
}
22 changes: 22 additions & 0 deletions GDJS/Runtime/Model3DManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,5 +162,27 @@ namespace gdjs {
this._invalidModel.scene.clear();
}
}

/**
* To be called when the scene is disposed.
* Clear the models, resources loaded and destroy 3D models loaders in this manager.
* @param resourcesList The list of specific resources
*/
disposeByResourcesList(resourcesList: ResourceData[]): void {
resourcesList.forEach((resourceData) => {
const loadedThreeModel = this._loadedThreeModels.get(resourceData);
if (loadedThreeModel) {
loadedThreeModel.scene.clear();
this._loadedThreeModels.delete(resourceData);
}

const downloadedArrayBuffer = this._downloadedArrayBuffers.get(
resourceData
);
if (downloadedArrayBuffer) {
this._downloadedArrayBuffers.delete(resourceData);
}
});
}
}
}
115 changes: 115 additions & 0 deletions GDJS/Runtime/ResourceLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ namespace gdjs {
* Keep track of which scene whose resources has already be loaded.
*/
private _sceneNamesToMakeReady: Set<string>;
/**
* Keep scenes names with unloaded assets
*/
private _scenesWithUnloadedAssets: Set<string>;
/**
* A queue of scenes whose resources are still to be pre-loaded.
*/
Expand Down Expand Up @@ -161,6 +165,7 @@ namespace gdjs {
this._sceneResources = new Map<string, Array<string>>();
this._sceneNamesToLoad = new Set<string>();
this._sceneNamesToMakeReady = new Set<string>();
this._scenesWithUnloadedAssets = new Set<string>();
this.setResources(resourceDataArray, globalResources, layoutDataArray);

this._imageManager = new gdjs.ImageManager(this);
Expand Down Expand Up @@ -343,6 +348,46 @@ namespace gdjs {
this.currentLoadingSceneName = '';
}

/**
* This method is used for scenes which was unloaded assets
* @param sceneName
* @param onProgress The callback for calculating in load progress
*/

async loadSceneResourcesBySceneName(
sceneName: string,
onProgress: (count: number, total: number) => void
): Promise<void> {
const sceneResources = this._sceneResources.get(sceneName);
if (!sceneResources) {
logger.warn(
'Can\'t load resource for unknown scene: "' + sceneName + '".'
);
return;
}
let loadedCount = 0;
const resources = [...sceneResources.values()];
await processAndRetryIfNeededWithPromisePool(
resources,
maxForegroundConcurrency,
maxAttempt,
async (resourceName) => {
const resource = this._resources.get(resourceName);
if (!resource) {
logger.warn('Unable to find resource "' + resourceName + '".');
return;
}
await this._loadResource(resource);
await this._processResource(resource);
loadedCount++;
onProgress(loadedCount, resources.length);
}
);
this._setSceneAssetsLoaded(sceneName);
this._setSceneAssetsReady(sceneName);
this._scenesWithUnloadedAssets.delete(sceneName);
}

private async _doLoadSceneResources(
sceneName: string,
onProgress?: (count: number, total: number) => Promise<void>
Expand Down Expand Up @@ -463,6 +508,30 @@ namespace gdjs {
}
}

/**
* To be called when the scene is disposed.
* Dispose all the scene resources managers.
* @param sceneName Scene name where need to clear all resources
*/
disposeScene(sceneName: string): void {
if (!sceneName) return;

const sceneUniqueResourcesByKindMap =
this._getSceneUniqueResourcesByKind(sceneName);

for (const [kindResourceManager, resourceManager] of this
._resourceManagersMap) {
const resources =
sceneUniqueResourcesByKindMap.get(kindResourceManager);
if (resources) {
resourceManager.disposeByResourcesList(resources);
}
}
this._scenesWithUnloadedAssets.add(sceneName);
this._sceneNamesToLoad.add(sceneName);
this._sceneNamesToMakeReady.add(sceneName);
}

/**
* Put a given scene at the end of the queue.
*
Expand Down Expand Up @@ -636,6 +705,52 @@ namespace gdjs {
getSpineAtlasManager(): gdjs.SpineAtlasManager | null {
return this._spineAtlasManager;
}

/**
* Get the Map of unique scene resources by resource type
* @param sceneName The scene name
* @return Map of unique scene resources by resource type
*/
_getSceneUniqueResourcesByKind(
sceneName: string
): Map<ResourceKind, ResourceData[]> {
if (!this._sceneResources.has(sceneName)) {
return new Map<ResourceKind, ResourceData[]>();
}

const resourceUsage: Map<string, number> = new Map();
for (const [key, resources] of this._sceneResources) {
// If some scene was unloaded, skip this scene's resources
if (this._scenesWithUnloadedAssets.has(key)) {
continue;
}
resources.forEach((resourceName) => {
resourceUsage.set(
resourceName,
(resourceUsage.get(resourceName) || 0) + 1
);
});
}

const sceneResourceNames = this._sceneResources.get(sceneName)!;
const uniqueResourceNames = sceneResourceNames.filter(
(resourceName) => resourceUsage.get(resourceName) === 1
);

const result = new Map<ResourceKind, ResourceData[]>();
uniqueResourceNames.forEach((resourceName) => {
const resourceData = this._resources.get(resourceName);
if (resourceData) {
const kind = resourceData.kind;
if (!result.has(kind)) {
result.set(kind, []);
}
result.get(kind)!.push(resourceData);
}
});

return result;
}
}

type PromiseError<T> = { item: T; error: Error };
Expand Down
7 changes: 7 additions & 0 deletions GDJS/Runtime/ResourceManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,12 @@ namespace gdjs {
* Using the manager after calling this method is undefined behavior.
*/
dispose(): void;

/**
* Should clear all specified in resourcesList resources, data, loaders stored by this manager.
* Using the manager after calling this method is undefined behavior.
* @param resourcesList The list of specific resources that need to be clear
*/
disposeByResourcesList(resourcesList: ResourceData[]): void;
}
}
4 changes: 2 additions & 2 deletions GDJS/Runtime/events-tools/runtimescenetools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ namespace gdjs {
export const replaceScene = function (
runtimeScene: gdjs.RuntimeScene,
newSceneName: string,
clearOthers: boolean
clearOthers: boolean,
) {
if (!runtimeScene.getGame().getSceneAndExtensionsData(newSceneName)) {
return;
Expand All @@ -249,7 +249,7 @@ namespace gdjs {
clearOthers
? gdjs.SceneChangeRequest.CLEAR_SCENES
: gdjs.SceneChangeRequest.REPLACE_SCENE,
newSceneName
newSceneName,
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,25 @@ namespace gdjs {
this._loadedFontFamily.clear();
this._loadedFontFamilySet.clear();
}

/**
* To be called when the scene is disposed.
* Clear caches of loaded font families.
* @param resourcesList The list of specific resources
*/
disposeByResourcesList(resourcesList: ResourceData[]): void {
resourcesList.forEach((resourceData) => {
const resource = this._loadedFontFamily.get(resourceData);
if (resource) {
this._loadedFontFamily.delete(resourceData);
}

const fontName = this._getFontFamilyFromFilename(resourceData);
if (fontName) {
this._loadedFontFamilySet.delete(fontName);
}
});
}
}

//Register the class to let the engine use it.
Expand Down
19 changes: 19 additions & 0 deletions GDJS/Runtime/howler-sound-manager/howler-sound-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,25 @@ namespace gdjs {
dispose(): void {
this.unloadAll();
}

/**
* To be called when the scene is disposed.
* Unloads all audio from resourcesList from memory, clear Howl cache and stop all audio.
* @param resourcesList The list of specific resources
*/
disposeByResourcesList(resourcesList: ResourceData[]): void {
resourcesList.forEach((resourceData) => {
const musicRes = this._loadedMusics.get(resourceData);
if (musicRes) {
this.unloadAudio(resourceData.name, true);
}

const soundRes = this._loadedSounds.get(resourceData);
if (soundRes) {
this.unloadAudio(resourceData.name, false);
}
});
}
}

// Register the class to let the engine use it.
Expand Down
19 changes: 19 additions & 0 deletions GDJS/Runtime/jsonmanager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,5 +208,24 @@ namespace gdjs {
this._loadedJsons.clear();
this._callbacks.clear();
}

/**
* To be called when the scene is disposed.
* Clear the JSONs loaded in this manager.
* @param resourcesList The list of specific resources
*/
disposeByResourcesList(resourcesList: ResourceData[]): void {
resourcesList.forEach((resourceData) => {
const loadedJson = this._loadedJsons.get(resourceData);
if (loadedJson) {
this._loadedJsons.delete(resourceData);
}

const callback = this._callbacks.get(resourceData);
if (callback) {
this._callbacks.delete(resourceData);
}
});
}
}
}
Loading