Files
esengine/packages/rendering/fairygui/src/render/RenderBridge.ts
YHH 155411e743 refactor: reorganize package structure and decouple framework packages (#338)
* refactor: reorganize package structure and decouple framework packages

## Package Structure Reorganization
- Reorganized 55 packages into categorized subdirectories:
  - packages/framework/ - Generic framework (Laya/Cocos compatible)
  - packages/engine/ - ESEngine core modules
  - packages/rendering/ - Rendering modules (WASM dependent)
  - packages/physics/ - Physics modules
  - packages/streaming/ - World streaming
  - packages/network-ext/ - Network extensions
  - packages/editor/ - Editor framework and plugins
  - packages/rust/ - Rust WASM engine
  - packages/tools/ - Build tools and SDK

## Framework Package Decoupling
- Decoupled behavior-tree and blueprint packages from ESEngine dependencies
- Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent)
- ESEngine-specific code moved to esengine/ subpath exports
- Framework packages now usable with Cocos/Laya without ESEngine

## CI Configuration
- Updated CI to only type-check and lint framework packages
- Added type-check:framework and lint:framework scripts

## Breaking Changes
- Package import paths changed due to directory reorganization
- ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine')

* fix: update es-engine file path after directory reorganization

* docs: update README to focus on framework over engine

* ci: only build framework packages, remove Rust/WASM dependencies

* fix: remove esengine subpath from behavior-tree and blueprint builds

ESEngine integration code will only be available in full engine builds.
Framework packages are now purely engine-agnostic.

* fix: move network-protocols to framework, build both in CI

* fix: update workflow paths from packages/core to packages/framework/core

* fix: exclude esengine folder from type-check in behavior-tree and blueprint

* fix: update network tsconfig references to new paths

* fix: add test:ci:framework to only test framework packages in CI

* fix: only build core and math npm packages in CI

* fix: exclude test files from CodeQL and fix string escaping security issue
2025-12-26 14:50:35 +08:00

311 lines
8.5 KiB
TypeScript

import type { IRectangle } from '../utils/MathTypes';
import type { IRenderCollector, IRenderPrimitive } from './IRenderCollector';
import type { IRenderBackend, IRenderStats, ITextureHandle, IFontHandle } from './IRenderBackend';
/**
* Texture cache entry
* 纹理缓存条目
*/
interface TextureCacheEntry {
handle: ITextureHandle;
lastUsedFrame: number;
refCount: number;
}
/**
* RenderBridge
*
* Bridges FairyGUI render primitives to the graphics backend.
* Provides batching, caching, and optimization.
*
* 将 FairyGUI 渲染图元桥接到图形后端
* 提供批处理、缓存和优化
*
* Features:
* - Automatic batching of similar primitives
* - Texture atlas support
* - Font caching
* - Render statistics
*
* @example
* ```typescript
* const bridge = new RenderBridge(webgpuBackend);
* await bridge.initialize(canvas);
*
* // In render loop
* bridge.beginFrame();
* root.collectRenderData(collector);
* bridge.render(collector);
* bridge.endFrame();
* ```
*/
export class RenderBridge {
private _backend: IRenderBackend;
private _textureCache: Map<string, TextureCacheEntry> = new Map();
private _fontCache: Map<string, IFontHandle> = new Map();
private _currentFrame: number = 0;
private _textureCacheMaxAge: number = 60; // Frames before texture is evicted
private _clipStack: IRectangle[] = [];
private _batchBuffer: IRenderPrimitive[] = [];
constructor(backend: IRenderBackend) {
this._backend = backend;
}
/**
* Get the underlying backend
* 获取底层后端
*/
public get backend(): IRenderBackend {
return this._backend;
}
/**
* Check if bridge is initialized
* 检查桥接是否已初始化
*/
public get isInitialized(): boolean {
return this._backend.isInitialized;
}
/**
* Initialize the bridge with a canvas
* 使用画布初始化桥接
*/
public async initialize(canvas: HTMLCanvasElement): Promise<boolean> {
return this._backend.initialize(canvas);
}
/**
* Begin a new frame
* 开始新帧
*/
public beginFrame(): void {
this._currentFrame++;
this._clipStack.length = 0;
this._batchBuffer.length = 0;
this._backend.beginFrame();
}
/**
* End the current frame
* 结束当前帧
*/
public endFrame(): void {
this.flushBatch();
this._backend.endFrame();
this.evictOldTextures();
}
/**
* Render primitives from a collector
* 渲染收集器中的图元
*/
public render(collector: IRenderCollector): void {
const primitives = collector.getPrimitives();
for (const primitive of primitives) {
this.processPrimitive(primitive);
}
}
/**
* Render a single primitive
* 渲染单个图元
*/
public renderPrimitive(primitive: IRenderPrimitive): void {
this.processPrimitive(primitive);
}
/**
* Push a clip rectangle
* 压入裁剪矩形
*/
public pushClipRect(rect: IRectangle): void {
if (this._clipStack.length > 0) {
const current = this._clipStack[this._clipStack.length - 1];
const intersected = this.intersectRects(current, rect);
this._clipStack.push(intersected);
} else {
this._clipStack.push({ ...rect });
}
this.flushBatch();
this._backend.setClipRect(this._clipStack[this._clipStack.length - 1]);
}
/**
* Pop the current clip rectangle
* 弹出当前裁剪矩形
*/
public popClipRect(): void {
if (this._clipStack.length > 0) {
this._clipStack.pop();
this.flushBatch();
this._backend.setClipRect(
this._clipStack.length > 0 ? this._clipStack[this._clipStack.length - 1] : null
);
}
}
/**
* Load or get cached texture
* 加载或获取缓存的纹理
*/
public async loadTexture(
url: string,
source?: ImageBitmap | HTMLImageElement | HTMLCanvasElement | ImageData
): Promise<ITextureHandle | null> {
// Check cache first
const cached = this._textureCache.get(url);
if (cached) {
cached.lastUsedFrame = this._currentFrame;
cached.refCount++;
return cached.handle;
}
// Load or create texture
let textureSource = source;
if (!textureSource) {
try {
const response = await fetch(url);
const blob = await response.blob();
textureSource = await createImageBitmap(blob);
} catch (error) {
console.error(`Failed to load texture: ${url}`, error);
return null;
}
}
const handle = this._backend.createTexture(textureSource);
this._textureCache.set(url, {
handle,
lastUsedFrame: this._currentFrame,
refCount: 1
});
return handle;
}
/**
* Release a texture reference
* 释放纹理引用
*/
public releaseTexture(url: string): void {
const cached = this._textureCache.get(url);
if (cached) {
cached.refCount--;
}
}
/**
* Load or get cached font
* 加载或获取缓存的字体
*/
public async loadFont(family: string, url?: string): Promise<IFontHandle> {
const cached = this._fontCache.get(family);
if (cached) {
return cached;
}
const handle = await this._backend.loadFont(family, url);
this._fontCache.set(family, handle);
return handle;
}
/**
* Resize the render target
* 调整渲染目标大小
*/
public resize(width: number, height: number): void {
this._backend.resize(width, height);
}
/**
* Get render statistics
* 获取渲染统计
*/
public getStats(): IRenderStats & { textureCount: number; fontCount: number } {
const backendStats = this._backend.getStats();
return {
...backendStats,
textureCount: this._textureCache.size,
fontCount: this._fontCache.size
};
}
/**
* Dispose the bridge and all resources
* 销毁桥接和所有资源
*/
public dispose(): void {
// Destroy all cached textures
for (const entry of this._textureCache.values()) {
this._backend.destroyTexture(entry.handle);
}
this._textureCache.clear();
this._fontCache.clear();
this._clipStack.length = 0;
this._batchBuffer.length = 0;
this._backend.dispose();
}
private processPrimitive(primitive: IRenderPrimitive): void {
// Check if can batch with previous primitives
if (this._batchBuffer.length > 0) {
const last = this._batchBuffer[this._batchBuffer.length - 1];
if (!this.canBatch(last, primitive)) {
this.flushBatch();
}
}
this._batchBuffer.push(primitive);
}
private canBatch(a: IRenderPrimitive, b: IRenderPrimitive): boolean {
// Can batch if same type and texture
if (a.type !== b.type) return false;
if (a.textureId !== b.textureId) return false;
if (a.blendMode !== b.blendMode) return false;
return true;
}
private flushBatch(): void {
if (this._batchBuffer.length === 0) return;
this._backend.submitPrimitives(this._batchBuffer);
this._batchBuffer.length = 0;
}
private evictOldTextures(): void {
const minFrame = this._currentFrame - this._textureCacheMaxAge;
const toEvict: string[] = [];
for (const [url, entry] of this._textureCache) {
if (entry.refCount <= 0 && entry.lastUsedFrame < minFrame) {
toEvict.push(url);
}
}
for (const url of toEvict) {
const entry = this._textureCache.get(url);
if (entry) {
this._backend.destroyTexture(entry.handle);
this._textureCache.delete(url);
}
}
}
private intersectRects(a: IRectangle, b: IRectangle): IRectangle {
const x = Math.max(a.x, b.x);
const y = Math.max(a.y, b.y);
const right = Math.min(a.x + a.width, b.x + b.width);
const bottom = Math.min(a.y + a.height, b.y + b.height);
return {
x,
y,
width: Math.max(0, right - x),
height: Math.max(0, bottom - y)
};
}
}