Skip to content
Merged
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
2 changes: 1 addition & 1 deletion e2e/case/project-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ WebGLEngine.create({ canvas: "canvas", shaderLab }).then((engine) => {
engine.resourceManager
.load({
type: AssetType.Project,
url: "https://mdn.alipayobjects.com/oasis_be/afts/file/A*fe1xS4Anh3AAAAAAQPAAAAgAekp5AQ/project.json"
url: "https://mdn.alipayobjects.com/oasis_be/afts/file/A*qGzdR5VaZToAAAAAQJAAAAgAekp5AQ/project.json"
})
.then(() => {
const cameraEntity = engine.sceneManager.activeScene.findEntityByName("Camera");
Expand Down
46 changes: 19 additions & 27 deletions packages/core/src/asset/ResourceManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -570,38 +570,33 @@ export class ResourceManager {
/** @internal */
_objectPool: { [key: string]: any } = Object.create(null);
/** @internal */
_idResourceMap: Record<ResourceId, EditorResourceItem> = Object.create(null);
/** @internal */
_virtualPathResourceMap: Record<VirtualPath, EditorResourceItem> = Object.create(null);

/**
* @internal
* @beta Just for internal editor, not recommended for developers.
*/
getResourceByRef<T extends EngineObject>(ref: { refId: string; key?: string; isClone?: boolean }): AssetPromise<T> {
const { refId, key, isClone } = ref;
const obj = this._objectPool[refId];
let promise: AssetPromise<T>;
if (obj) {
promise = AssetPromise.resolve(obj);
} else {
const resourceConfig = this._idResourceMap[refId];
if (!resourceConfig) {
Logger.warn(`refId:${refId} is not find in this._idResourceMap.`);
return AssetPromise.resolve(null);
}
let url = resourceConfig.virtualPath;
if (key) {
url += "?q=" + key;
}
getResourceByRef<T extends EngineObject>(ref: { url: string; key?: string; isClone?: boolean }): AssetPromise<T> {
const { url, key, isClone } = ref;
if (!url) {
Logger.warn("ResourceManager.getResourceByRef: url is empty.");
return AssetPromise.resolve(null);
}

promise = this.load<T>({
url,
type: resourceConfig.type,
params: resourceConfig.params
});
const cached = this._objectPool[url];
if (cached) {
return AssetPromise.resolve(isClone ? <T>(<IClone>(<unknown>cached)).clone() : cached);
}
return promise.then((item) => (isClone ? <T>(<IClone>(<unknown>item)).clone() : item));

const mapped = this._virtualPathResourceMap[url];
if (!mapped) {
Logger.warn(`ResourceManager.getResourceByRef: url "${url}" not found in virtualPathResourceMap.`);
return AssetPromise.resolve(null);
}

const loadUrl = key ? url + "?q=" + key : url;
const promise = this.load<T>({ url: loadUrl, type: mapped.type, params: mapped.params });
return isClone ? promise.then((item) => <T>(<IClone>(<unknown>item)).clone()) : promise;
Comment on lines +586 to +599
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Treat key as part of the cached ref.

packages/loader/src/resource-deserialize/resources/schema/BasicSchema.ts:104 now defines refs as { url, key? }, so key is the only discriminator for sibling sub-assets. The fast path still returns _objectPool[url] directly, which can make { url: "foo", key: "textures[0]" } and { url: "foo", key: "textures[1]" } resolve to the same cached object once the pool is warm. Canonicalize the full keyed URL and use it consistently for cache lookup/loading, or resolve the keyed sub-resource from the cached root before returning.

🛠️ Proposed fix
-    const cached = this._objectPool[url];
+    const fullUrl = key
+      ? `${url}${url.includes("?") ? "&" : "?"}q=${encodeURIComponent(key)}`
+      : url;
+    const cachedRoot = this._objectPool[url];
+    const cached =
+      this._objectPool[fullUrl] ??
+      (key && cachedRoot ? this._getResolveResource(cachedRoot, this._parseQueryPath(key)) : cachedRoot);
     if (cached) {
       return isClone ? AssetPromise.resolve(<T>(<IClone>(<unknown>cached)).clone()) : AssetPromise.resolve(cached);
     }
-    let loadUrl = url;
-    if (key) {
-      loadUrl += "?q=" + key;
-    }
+    const loadUrl = fullUrl;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/asset/ResourceManager.ts` around lines 589 - 603, The
fast-path ignores the optional key and can return the wrong cached sibling; fix
by canonicalizing a cacheKey = key ? `${url}?q=${key}` : url and use cacheKey
for the _objectPool lookup, for constructing loadUrl, and when accessing
_virtualPathResourceMap and calling this.load; keep the clone logic (IClone /
AssetPromise.resolve and promise.then clone) but operate on the cached value
found via cacheKey so `{url, key}` are treated as distinct entries.

}

/**
Expand All @@ -611,7 +606,6 @@ export class ResourceManager {
initVirtualResources(config: EditorResourceItem[]): void {
config.forEach((element) => {
this._virtualPathResourceMap[element.virtualPath] = element;
this._idResourceMap[element.id] = element;
if (element.dependentAssetMap) {
this._virtualPathResourceMap[element.virtualPath].dependentAssetMap = element.dependentAssetMap;
}
Expand Down Expand Up @@ -653,13 +647,11 @@ const rePropName = RegExp(
"g"
);

type ResourceId = string;
type VirtualPath = string;
type EditorResourceItem = {
virtualPath: string;
path: string;
type: string;
id: string;
dependentAssetMap?: { [key: string]: string };
subpackageName?: string;
params?: Record<string, any>;
Expand Down
4 changes: 2 additions & 2 deletions packages/loader/src/AnimationClipLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from "@galacean/engine-core";
import { decode } from "./resource-deserialize";

@resourceLoader(AssetType.AnimationClip, ["ani"])
@resourceLoader(AssetType.AnimationClip, ["anim"])
class AnimationClipLoader extends Loader<AnimationClip> {
load(item: LoadItem, resourceManager: ResourceManager): AssetPromise<AnimationClip> {
return new AssetPromise((resolve, reject) => {
Expand Down Expand Up @@ -45,7 +45,7 @@ class AnimationClipLoader extends Loader<AnimationClip> {
private _parseKeyframeValue(keyframe: any, resourceManager: ResourceManager): Promise<any> {
const value = keyframe.value;

if (typeof value === "object" && (value as any)?.refId) {
if (typeof value === "object" && (value as any)?.url) {
return new Promise((resolve) => {
resourceManager
// @ts-ignore
Expand Down
2 changes: 1 addition & 1 deletion packages/loader/src/AnimatorControllerLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
WrapMode
} from "@galacean/engine-core";

@resourceLoader(AssetType.AnimatorController, ["json"], false)
@resourceLoader(AssetType.AnimatorController, ["animCtrl"])
class AnimatorControllerLoader extends Loader<AnimatorController> {
load(item: LoadItem, resourceManager: ResourceManager): AssetPromise<AnimatorController> {
return new AssetPromise((resolve, reject) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/loader/src/BufferLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {

const base64Regex = /^data:(.+?);base64,/;

@resourceLoader(AssetType.Buffer, ["bin", "r3bin"])
@resourceLoader(AssetType.Buffer, ["bin"])
class BufferLoader extends Loader<BufferAsset> {
load(item: LoadItem, resourceManager: ResourceManager): AssetPromise<BufferAsset> {
const { url } = item;
Expand Down
2 changes: 1 addition & 1 deletion packages/loader/src/MaterialLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ function parseProperty(object: Object, key: string, value: any) {
}
}

@resourceLoader(AssetType.Material, ["json"])
@resourceLoader(AssetType.Material, ["mat"])
class MaterialLoader extends Loader<Material> {
load(item: LoadItem, resourceManager: ResourceManager): AssetPromise<Material> {
return new AssetPromise((resolve, reject) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/loader/src/PhysicsMaterialLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
PhysicsMaterial
} from "@galacean/engine-core";

@resourceLoader(AssetType.PhysicsMaterial, ["mesh"])
@resourceLoader(AssetType.PhysicsMaterial, ["physMat"])
class PhysicsMaterialLoader extends Loader<PhysicsMaterial> {
load(item: LoadItem, resourceManager: ResourceManager): AssetPromise<PhysicsMaterial> {
return (
Expand Down
2 changes: 1 addition & 1 deletion packages/loader/src/ProjectLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from "@galacean/engine-core";
import { IProject } from "./resource-deserialize";

@resourceLoader(AssetType.Project, ["proj"], false)
@resourceLoader(AssetType.Project, ["project"], false)
class ProjectLoader extends Loader<void> {
load(item: LoadItem, resourceManager: ResourceManager): AssetPromise<void> {
const { engine } = resourceManager;
Expand Down
23 changes: 0 additions & 23 deletions packages/loader/src/Texture2DContentRestorer.ts

This file was deleted.

143 changes: 98 additions & 45 deletions packages/loader/src/Texture2DLoader.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
AssetPromise,
AssetType,
ContentRestorer,
LoadItem,
Loader,
RequestConfig,
Expand All @@ -12,65 +13,117 @@ import {
TextureWrapMode,
resourceLoader
} from "@galacean/engine-core";
import { Texture2DContentRestorer } from "./Texture2DContentRestorer";
import { decode } from "./resource-deserialize";
import { FileHeader } from "./resource-deserialize/utils/FileHeader";

@resourceLoader(AssetType.Texture2D, ["png", "jpg", "webp", "jpeg"])
function loadImageFromBuffer(buffer: ArrayBuffer): AssetPromise<HTMLImageElement> {
return new AssetPromise((resolve, reject) => {
const blob = new Blob([buffer]);
const img = new Image();
img.onload = () => {
URL.revokeObjectURL(img.src);
resolve(img);
};
img.onerror = reject;
img.src = URL.createObjectURL(blob);
});
Comment on lines +19 to +29
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Revoke the blob URL on onerror too.

The helper only releases the object URL in onload. Failed image decodes leak the blob URL, and this helper is shared by both load and restore paths.

🧹 Suggested fix
 function loadImageFromBuffer(buffer: ArrayBuffer): AssetPromise<HTMLImageElement> {
   return new AssetPromise((resolve, reject) => {
     const blob = new Blob([buffer]);
     const img = new Image();
+    const objectURL = URL.createObjectURL(blob);
     img.onload = () => {
-      URL.revokeObjectURL(img.src);
+      URL.revokeObjectURL(objectURL);
       resolve(img);
     };
-    img.onerror = reject;
-    img.src = URL.createObjectURL(blob);
+    img.onerror = (event) => {
+      URL.revokeObjectURL(objectURL);
+      reject(event);
+    };
+    img.src = objectURL;
   });
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function loadImageFromBuffer(buffer: ArrayBuffer): AssetPromise<HTMLImageElement> {
return new AssetPromise((resolve, reject) => {
const blob = new Blob([buffer]);
const img = new Image();
img.onload = () => {
URL.revokeObjectURL(img.src);
resolve(img);
};
img.onerror = reject;
img.src = URL.createObjectURL(blob);
});
function loadImageFromBuffer(buffer: ArrayBuffer): AssetPromise<HTMLImageElement> {
return new AssetPromise((resolve, reject) => {
const blob = new Blob([buffer]);
const img = new Image();
const objectURL = URL.createObjectURL(blob);
img.onload = () => {
URL.revokeObjectURL(objectURL);
resolve(img);
};
img.onerror = (event) => {
URL.revokeObjectURL(objectURL);
reject(event);
};
img.src = objectURL;
});
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/loader/src/Texture2DLoader.ts` around lines 19 - 29,
loadImageFromBuffer leaks the created object URL on image decode failures
because URL.revokeObjectURL is only called in img.onload; modify
loadImageFromBuffer so that the blob URL created by URL.createObjectURL(blob) is
stored in a local variable and revoked both in img.onload and in img.onerror
(and ensure img.onerror calls reject after revoking). Update references to
img.src to use that local url variable when calling revoke in both handlers to
guarantee the URL is always released.

}

@resourceLoader(AssetType.Texture2D, ["png", "jpg", "webp", "jpeg", "tex"])
class Texture2DLoader extends Loader<Texture2D> {
override load(item: LoadItem, resourceManager: ResourceManager): AssetPromise<Texture2D> {
const url = item.url;
const requestConfig = <RequestConfig>{ ...item, type: "arraybuffer" };
return new AssetPromise((resolve, reject, setTaskCompleteProgress, setTaskDetailProgress) => {
const url = item.url;
const requestConfig = <RequestConfig>{
...item,
type: "image"
};
resourceManager
// @ts-ignore
._request<HTMLImageElement>(url, requestConfig)
._request<ArrayBuffer>(url, requestConfig)
.onProgress(setTaskCompleteProgress, setTaskDetailProgress)
.then((image) => {
const {
format = TextureFormat.R8G8B8A8,
anisoLevel,
wrapModeU,
wrapModeV,
filterMode,
isSRGBColorSpace = true,
mipmap = true
} = (item.params as Partial<Texture2DParams>) ?? {};
const { width, height } = image;
const engine = resourceManager.engine;
.then((buffer) => {
if (FileHeader.checkMagic(buffer)) {
decode<Texture2D>(buffer, resourceManager.engine).then((texture) => {
resourceManager.addContentRestorer(new Texture2DContentRestorer(texture, url, requestConfig));
resolve(texture);
}, reject);
} else {
loadImageFromBuffer(buffer).then((img) => {
Comment on lines +43 to +49
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep the no-magic serialized-texture path reachable.

packages/loader/src/resource-deserialize/utils/FileHeader.ts now decodes headers with or without MAGIC, but these branches only call decode() when checkMagic() is true. That makes the no-magic path unreachable here, so serialized texture buffers without the prefix fall through to loadImageFromBuffer() instead of the binary decoder.

💡 One straightforward fix
-          if (FileHeader.checkMagic(buffer)) {
+          const isSerializedTexture = FileHeader.checkMagic(buffer) || /\.tex(?:$|[?#])/i.test(url);
+          if (isSerializedTexture) {
-          if (FileHeader.checkMagic(buffer)) {
+          const isSerializedTexture = FileHeader.checkMagic(buffer) || /\.tex(?:$|[?#])/i.test(this.url);
+          if (isSerializedTexture) {

Also applies to: 111-114

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/loader/src/Texture2DLoader.ts` around lines 43 - 49, The current
branch only calls decode<Texture2D> when FileHeader.checkMagic(buffer) is true,
which prevents serialized-but-no-MAGIC textures from being decoded; change the
condition to ask FileHeader's new header-detection/parsing API (the function you
added that recognizes headers with or without MAGIC) and if that parser reports
the buffer is a serialized texture, call decode<Texture2D>(buffer,
resourceManager.engine). After decoding, continue to add the restorer via
resourceManager.addContentRestorer(new Texture2DContentRestorer(...)) and
resolve, otherwise fall back to loadImageFromBuffer(buffer) as before.

const texture = this._createTexture(img, item, resourceManager);
resourceManager.addContentRestorer(new Texture2DContentRestorer(texture, url, requestConfig));
resolve(texture);
}, reject);
}
})
.catch(reject);
});
}

const generateMipmap = TextureUtils.supportGenerateMipmapsWithCorrection(
engine,
width,
height,
format,
mipmap,
isSRGBColorSpace
);
private _createTexture(img: HTMLImageElement, item: LoadItem, resourceManager: ResourceManager): Texture2D {
const {
format = TextureFormat.R8G8B8A8,
anisoLevel,
wrapModeU,
wrapModeV,
filterMode,
isSRGBColorSpace = true,
mipmap = true
} = (item.params as Partial<Texture2DParams>) ?? {};
const { width, height } = img;
const engine = resourceManager.engine;

const texture = new Texture2D(engine, width, height, format, generateMipmap, isSRGBColorSpace);
const generateMipmap = TextureUtils.supportGenerateMipmapsWithCorrection(
engine,
width,
height,
format,
mipmap,
isSRGBColorSpace
);

texture.anisoLevel = anisoLevel ?? texture.anisoLevel;
texture.filterMode = filterMode ?? texture.filterMode;
texture.wrapModeU = wrapModeU ?? texture.wrapModeU;
texture.wrapModeV = wrapModeV ?? texture.wrapModeV;
const texture = new Texture2D(engine, width, height, format, generateMipmap, isSRGBColorSpace);
texture.anisoLevel = anisoLevel ?? texture.anisoLevel;
texture.filterMode = filterMode ?? texture.filterMode;
texture.wrapModeU = wrapModeU ?? texture.wrapModeU;
texture.wrapModeV = wrapModeV ?? texture.wrapModeV;
texture.setImageSource(img);
generateMipmap && texture.generateMipmaps();

texture.setImageSource(image);
generateMipmap && texture.generateMipmaps();
const url = item.url;
if (url.indexOf("data:") !== 0) {
texture.name = url.substring(url.lastIndexOf("/") + 1);
}

if (url.indexOf("data:") !== 0) {
const index = url.lastIndexOf("/");
texture.name = url.substring(index + 1);
}
return texture;
}
}

class Texture2DContentRestorer extends ContentRestorer<Texture2D> {
constructor(
resource: Texture2D,
public url: string,
public requestConfig: RequestConfig
) {
super(resource);
}

resourceManager.addContentRestorer(new Texture2DContentRestorer(texture, url, requestConfig));
resolve(texture);
override restoreContent(): AssetPromise<Texture2D> {
const texture = this.resource;
const engine = texture.engine;
return (
engine.resourceManager
// @ts-ignore
._request<ArrayBuffer>(this.url, this.requestConfig)
.then((buffer) => {
if (FileHeader.checkMagic(buffer)) {
return decode<Texture2D>(buffer, engine, texture);
} else {
return loadImageFromBuffer(buffer).then((img) => {
texture.setImageSource(img);
texture.generateMipmaps();
return texture;
});
}
})
.catch((e) => {
reject(e);
});
});
);
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/loader/src/TextureCubeLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
} from "@galacean/engine-core";
import { TextureCubeContentRestorer } from "./TextureCubeContentRestorer";

@resourceLoader(AssetType.TextureCube, [""])
@resourceLoader(AssetType.TextureCube, ["texCube"])
class TextureCubeLoader extends Loader<TextureCube> {
override load(item: LoadItem, resourceManager: ResourceManager): AssetPromise<TextureCube> {
return new AssetPromise((resolve, reject) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/loader/src/gltf/extensions/GLTFExtensionSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export interface IEXTMeshoptCompressionSchema {
}

export interface IGalaceanMaterialRemap {
refId: string;
url: string;
key?: string;
isClone?: boolean;
}
Expand Down
1 change: 0 additions & 1 deletion packages/loader/src/resource-deserialize/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ export function decode<T>(arrayBuffer: ArrayBuffer, engine: Engine, ...args: any

export * from "./resources/parser/HierarchyParser";
export * from "./resources/parser/ParserContext";
export * from "./resources/scene/EditorTextureLoader";
export * from "./resources/scene/SceneParser";
export * from "./resources/schema";

Expand Down
Loading
Loading