Skip to content

Commit

Permalink
Support binary data in asset pack, support typed array in compiled se…
Browse files Browse the repository at this point in the history
…rialization (#15904)

* CCON pack

* Apply review suggestion

* Apply review suggestion 2

* When unpacking, copy section binary instead of referencing

* Optimize the case section has no binary storage

* Tweak `PackManager.pack` signature

* Optimize typed array type index
  • Loading branch information
shrinktofit authored Sep 8, 2023
1 parent 10ba495 commit 4381c62
Show file tree
Hide file tree
Showing 5 changed files with 259 additions and 14 deletions.
14 changes: 11 additions & 3 deletions cocos/asset/asset-manager/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ export default class Config {
this._initUuid(options.uuids);
this._initPath(options.paths);
this._initScene(options.scenes);
this._initPackage(options.packs);
this._initPackage(options.packs, options.extensionMap);
this._initVersion(options.versions);
this._initRedirect(options.redirect);
for (const ext in options.extensionMap) {
Expand Down Expand Up @@ -382,12 +382,20 @@ export default class Config {
}
}

private _initPackage (packageList: Record<string, string[]>): void {
private _initPackage (packageList: Record<string, string[]>, extensionMap: IConfigOption['extensionMap']): void {
if (!packageList) { return; }
const assetInfos = this.assetInfos;
for (const packUuid in packageList) {
const uuids = packageList[packUuid];
const pack = { uuid: packUuid, packedUuids: uuids, ext: '.json' };
let mappedExtension = '.json';
for (const ext in extensionMap) {
const mappedUUIDs = extensionMap[ext];
if (mappedUUIDs.includes(packUuid)) {
mappedExtension = ext;
break;
}
}
const pack = { uuid: packUuid, packedUuids: uuids, ext: mappedExtension };
assetInfos.add(packUuid, pack);

for (let i = 0, l = uuids.length; i < l; i++) {
Expand Down
13 changes: 7 additions & 6 deletions cocos/asset/asset-manager/pack-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

import { ImageAsset } from '../assets/image-asset';
import { Texture2D } from '../assets/texture-2d';
import { packCustomObjData, unpackJSONs } from '../../serialization/deserialize';
import { isGeneralPurposePack, packCustomObjData, unpackJSONs } from '../../serialization/deserialize';
import { assertIsTrue, error, errorID, js } from '../../core';
import Cache from './cache';
import downloader from './downloader';
Expand Down Expand Up @@ -56,6 +56,7 @@ export class PackManager {
private _loading = new Cache<IUnpackRequest[]>();
private _unpackers: Record<string, Unpacker> = {
'.json': this.unpackJson,
'.ccon': this.unpackJson,
};

/**
Expand All @@ -79,22 +80,22 @@ export class PackManager {
*
*/
public unpackJson (
pack: string[],
pack: readonly string[],
json: any,
options: Record<string, any>,
onComplete: ((err: Error | null, data?: Record<string, any> | null) => void),
): void {
const out: Record<string, any> = js.createMap(true);
let err: Error | null = null;

if (Array.isArray(json)) {
json = unpackJSONs(json as unknown as Parameters<typeof unpackJSONs>[0]);
if (isGeneralPurposePack(json)) {
const unpacked = unpackJSONs(json);

if (json.length !== pack.length) {
if (unpacked.length !== pack.length) {
errorID(4915);
}
for (let i = 0; i < pack.length; i++) {
out[`${pack[i]}@import`] = json[i];
out[`${pack[i]}@import`] = unpacked[i];
}
} else {
const textureType = js.getClassId(Texture2D);
Expand Down
129 changes: 129 additions & 0 deletions cocos/serialization/compiled/typed-array.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { assertIsTrue, sys } from '../../core';
import { IRuntimeFileData } from '../deserialize';

assertIsTrue(sys.isLittleEndian, `Deserialization system currently suppose little endian.`);

export const typedArrayTypeTable = Object.freeze([
Float32Array,
Float64Array,

Int8Array,
Int16Array,
Int32Array,

Uint8Array,
Uint16Array,
Uint32Array,

Uint8ClampedArray,
// BigInt64Array,
// BigUint64Array,
] as const);

/**
* Describes the serialized data of an typed array.
* - If it's an array, it's `TypedArrayDataJson`.
* - Otherwise, it's `TypedArrayDataPtr`.
*/
export type TypedArrayData = TypedArrayDataJson | TypedArrayDataPtr;

export type TypedArrayDataJson = [
/**
* Indicates the constructor of typed array.
* It's index of the constructor in `TypedArrays`.
*/
typeIndex: number,

/**
* Array element values.
*/
elements: number[],
];

/**
* Let `offset` be this value,
* Let `storage` be the binary buffer attached to the deserialized document.
* Then, the data of `storage` started from `offset`
* can be described using the following structure(in C++, assuming fields are packed tightly):
*
* ```cpp
* struct _ {
* /// Indicates the constructor of typed array.
* /// It's index of the constructor in `typedArrayTypeTable`.
* std::uint32_t typeIndex;
*
* /// The typed array's element count. Note this is not "byte length".
* std:: uint32_t length;
*
* /// Automatically padding bytes to align the `arrayBufferBytes`.
* /// See comments on `arrayBufferBytes`.
* std::byte[] _padding;
*
* /// Bytes of the underlying `ArrayBuffer` of this typed array.
* /// Should be aligned to `typedArrayConstructor.BYTES_PER_ELEMENT`
* /// according to https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray#bytelength_must_be_aligned.
* std::byte[] arrayBufferBytes;
* }
* ```
*/
export type TypedArrayDataPtr = number;

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
function getTypedArrayConstructor (typeIndex: number) {
assertIsTrue(typeIndex >= 0 && typeIndex < typedArrayTypeTable.length);
return typedArrayTypeTable[typeIndex];
}

function calculatePaddingToAlignAs (v: number, align: number): number {
if (align === 0) {
return 0;
}
const remainder = v % align;
if (remainder !== 0) {
return align - remainder;
}
return 0;
}

function decodeTypedArray (data: IRuntimeFileData, value: TypedArrayData): ArrayBufferView {
if (Array.isArray(value)) {
const [typeIndex, elements] = value;
const TypedArrayConstructor = getTypedArrayConstructor(typeIndex);
return new TypedArrayConstructor(elements);
} else {
const context = data[0];
const attachedBinary = context._attachedBinary;
assertIsTrue(attachedBinary, `Incorrect data: binary is expected.`);
const dataView = (context._attachedBinaryDataViewCache
??= new DataView(attachedBinary.buffer, attachedBinary.byteOffset, attachedBinary.byteLength));

let p = value;
const header = dataView.getUint8(p);
p += 1;
const length = dataView.getUint32(p, true);
p += 4;

const typeIndex = header & 0xFF;
const TypedArrayConstructor = getTypedArrayConstructor(typeIndex);

// The elements must be padded.
p += calculatePaddingToAlignAs(p + attachedBinary.byteOffset, TypedArrayConstructor.BYTES_PER_ELEMENT);

// Copy the section:
// - Allocates the result.
// - Creates a view on big buffer.
// - Copy using `TypedArray.prototype.set`.
// This manner do not consider the endianness problem.
//
// Here listed the benchmark in various other ways:
// https://jsperf.app/vayeri/2/preview
//
const result = new TypedArrayConstructor(length);
result.set(new TypedArrayConstructor(attachedBinary.buffer, attachedBinary.byteOffset + p, length));
return result;
}
}

export function deserializeTypedArray (data: IRuntimeFileData, owner: any, key: string, value: TypedArrayData): void {
owner[key] = decodeTypedArray(data, value);
}
Loading

0 comments on commit 4381c62

Please sign in to comment.