From 75be905f14715d75bebf2fc1088e9a79171e54f7 Mon Sep 17 00:00:00 2001 From: yhh <359807859@qq.com> Date: Tue, 16 Dec 2025 11:25:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(engine):=20=E6=94=B9=E8=BF=9B=20Rust=20?= =?UTF-8?q?=E7=BA=B9=E7=90=86=E7=AE=A1=E7=90=86=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 支持任意 ID 的纹理加载(非递增) - 添加纹理状态追踪 API - 优化纹理缓存清理机制 - 更新 TypeScript 绑定 --- .../src/core/EngineBridge.ts | 127 ++++++++++++++++++ .../src/systems/EngineRenderSystem.ts | 33 +++-- .../src/wasm/es_engine.d.ts | 28 ++++ packages/engine/src/core/engine.rs | 26 ++-- packages/engine/src/lib.rs | 36 +++++ packages/engine/src/renderer/texture/mod.rs | 2 +- .../src/renderer/texture/texture_manager.rs | 111 ++++++++++++++- 7 files changed, 335 insertions(+), 28 deletions(-) diff --git a/packages/ecs-engine-bindgen/src/core/EngineBridge.ts b/packages/ecs-engine-bindgen/src/core/EngineBridge.ts index 2095ccbb..4e22aad0 100644 --- a/packages/ecs-engine-bindgen/src/core/EngineBridge.ts +++ b/packages/ecs-engine-bindgen/src/core/EngineBridge.ts @@ -883,6 +883,133 @@ export class EngineBridge implements ITextureEngineBridge { this.getEngine().clearAllTextures(); } + // ===== Texture State API ===== + // ===== 纹理状态 API ===== + + /** + * Get texture loading state. + * 获取纹理加载状态。 + * + * @param id - Texture ID | 纹理ID + * @returns State string: 'loading', 'ready', or 'failed:reason' + * 状态字符串:'loading'、'ready' 或 'failed:reason' + */ + getTextureState(id: number): string { + if (!this.initialized) return 'loading'; + return this.getEngine().getTextureState(id); + } + + /** + * Check if texture is ready for rendering. + * 检查纹理是否已就绪可渲染。 + * + * @param id - Texture ID | 纹理ID + * @returns true if texture data is fully loaded | 纹理数据完全加载则返回true + */ + isTextureReady(id: number): boolean { + if (!this.initialized) return false; + return this.getEngine().isTextureReady(id); + } + + /** + * Get count of textures currently loading. + * 获取当前正在加载的纹理数量。 + * + * @returns Number of textures in 'loading' state | 处于加载状态的纹理数量 + */ + getTextureLoadingCount(): number { + if (!this.initialized) return 0; + return this.getEngine().getTextureLoadingCount(); + } + + /** + * Load texture asynchronously with Promise. + * 使用Promise异步加载纹理。 + * + * Unlike loadTexture which returns immediately with a placeholder, + * this method waits until the texture is actually loaded and ready. + * 与loadTexture立即返回占位符不同,此方法会等待纹理实际加载完成。 + * + * @param id - Texture ID | 纹理ID + * @param url - Image URL | 图片URL + * @returns Promise that resolves when texture is ready, rejects on failure + * 纹理就绪时解析的Promise,失败时拒绝 + */ + loadTextureAsync(id: number, url: string): Promise { + return new Promise((resolve, reject) => { + if (!this.initialized) { + reject(new Error('Engine not initialized')); + return; + } + + // Start loading the texture + // 开始加载纹理 + this.getEngine().loadTexture(id, url); + + // Poll for state changes + // 轮询状态变化 + const checkInterval = 16; // ~60fps + const maxWaitTime = 30000; // 30 seconds timeout + let elapsed = 0; + + const checkState = () => { + const state = this.getTextureState(id); + + if (state === 'ready') { + resolve(); + } else if (state.startsWith('failed:')) { + const reason = state.substring(7); + reject(new Error(`Texture load failed: ${reason}`)); + } else if (elapsed >= maxWaitTime) { + reject(new Error(`Texture load timeout after ${maxWaitTime}ms`)); + } else { + elapsed += checkInterval; + setTimeout(checkState, checkInterval); + } + }; + + // Start checking after a small delay to allow initial state setup + // 稍后开始检查,允许初始状态设置 + setTimeout(checkState, checkInterval); + }); + } + + /** + * Wait for all loading textures to complete. + * 等待所有加载中的纹理完成。 + * + * @param timeout - Maximum wait time in ms (default: 30000) + * 最大等待时间(毫秒,默认30000) + * @returns Promise that resolves when all textures are loaded + * 所有纹理加载完成时解析的Promise + */ + waitForAllTextures(timeout: number = 30000): Promise { + return new Promise((resolve, reject) => { + if (!this.initialized) { + reject(new Error('Engine not initialized')); + return; + } + + const checkInterval = 16; + let elapsed = 0; + + const checkLoading = () => { + const loadingCount = this.getTextureLoadingCount(); + + if (loadingCount === 0) { + resolve(); + } else if (elapsed >= timeout) { + reject(new Error(`Timeout waiting for ${loadingCount} textures to load`)); + } else { + elapsed += checkInterval; + setTimeout(checkLoading, checkInterval); + } + }; + + checkLoading(); + }); + } + /** * Dispose the bridge and release resources. * 销毁桥接并释放资源。 diff --git a/packages/ecs-engine-bindgen/src/systems/EngineRenderSystem.ts b/packages/ecs-engine-bindgen/src/systems/EngineRenderSystem.ts index 76a8bb7f..cf288be1 100644 --- a/packages/ecs-engine-bindgen/src/systems/EngineRenderSystem.ts +++ b/packages/ecs-engine-bindgen/src/systems/EngineRenderSystem.ts @@ -3,16 +3,16 @@ * 用于ECS的引擎渲染系统。 */ -import { EntitySystem, Matcher, Entity, ComponentType, ECSSystem, Component, Core } from '@esengine/ecs-framework'; -import { TransformComponent, sortingLayerManager } from '@esengine/engine-core'; -import { Color } from '@esengine/ecs-framework-math'; -import { SpriteComponent } from '@esengine/sprite'; import { CameraComponent } from '@esengine/camera'; +import { Component, ComponentType, Core, ECSSystem, Entity, EntitySystem, Matcher } from '@esengine/ecs-framework'; +import { Color } from '@esengine/ecs-framework-math'; +import { TransformComponent, sortingLayerManager } from '@esengine/engine-core'; import { getMaterialManager } from '@esengine/material-system'; +import { SpriteComponent } from '@esengine/sprite'; import type { EngineBridge } from '../core/EngineBridge'; import { RenderBatcher } from '../core/RenderBatcher'; -import type { SpriteRenderData } from '../types'; import type { ITransformComponent } from '../core/SpriteRenderHelper'; +import type { SpriteRenderData } from '../types'; /** * Render data from a provider @@ -339,14 +339,12 @@ export class EngineRenderSystem extends EntitySystem { } // Calculate UV with flip | 计算带翻转的 UV - const uv: [number, number, number, number] = [0, 0, 1, 1]; - if (sprite.flipX || sprite.flipY) { - if (sprite.flipX) { - [uv[0], uv[2]] = [uv[2], uv[0]]; - } - if (sprite.flipY) { - [uv[1], uv[3]] = [uv[3], uv[1]]; - } + const uv: [number, number, number, number] = [...sprite.uv]; + if (sprite.flipX) { + [uv[0], uv[2]] = [uv[2], uv[0]]; + } + if (sprite.flipY) { + [uv[1], uv[3]] = [uv[3], uv[1]]; } // 使用世界变换(由 TransformSystem 计算,考虑父级变换),回退到本地变换 @@ -569,6 +567,13 @@ export class EngineRenderSystem extends EntitySystem { const tOffset = i * 7; const uvOffset = i * 4; + const uv: [number, number, number, number] = [ + data.uvs[uvOffset], + data.uvs[uvOffset + 1], + data.uvs[uvOffset + 2], + data.uvs[uvOffset + 3] + ]; + const renderData: SpriteRenderData = { x: data.transforms[tOffset], y: data.transforms[tOffset + 1], @@ -578,7 +583,7 @@ export class EngineRenderSystem extends EntitySystem { originX: data.transforms[tOffset + 5], originY: data.transforms[tOffset + 6], textureId, - uv: [data.uvs[uvOffset], data.uvs[uvOffset + 1], data.uvs[uvOffset + 2], data.uvs[uvOffset + 3]], + uv, color: data.colors[i] }; diff --git a/packages/ecs-engine-bindgen/src/wasm/es_engine.d.ts b/packages/ecs-engine-bindgen/src/wasm/es_engine.d.ts index bfe92d84..8896ac1c 100644 --- a/packages/ecs-engine-bindgen/src/wasm/es_engine.d.ts +++ b/packages/ecs-engine-bindgen/src/wasm/es_engine.d.ts @@ -209,11 +209,31 @@ export class GameEngine { * 获取所有已注册的视口ID。 */ getViewportIds(): string[]; + /** + * 检查纹理是否已就绪 + * Check if texture is ready to use + * + * # Arguments | 参数 + * * `id` - Texture ID | 纹理ID + */ + isTextureReady(id: number): boolean; /** * Add a capsule gizmo outline. * 添加胶囊Gizmo边框。 */ addGizmoCapsule(x: number, y: number, radius: number, half_height: number, rotation: number, r: number, g: number, b: number, a: number): void; + /** + * 获取纹理加载状态 + * Get texture loading state + * + * # Arguments | 参数 + * * `id` - Texture ID | 纹理ID + * + * # Returns | 返回 + * State string: "loading", "ready", or "failed:reason" + * 状态字符串:"loading"、"ready" 或 "failed:原因" + */ + getTextureState(id: number): string; /** * Register a new viewport. * 注册新视口。 @@ -361,6 +381,11 @@ export class GameEngine { * 在恢复场景快照时应调用此方法,以确保纹理使用正确的ID重新加载。 */ clearTexturePathCache(): void; + /** + * 获取正在加载中的纹理数量 + * Get the number of textures currently loading + */ + getTextureLoadingCount(): number; /** * Create a new game engine instance. * 创建新的游戏引擎实例。 @@ -429,6 +454,8 @@ export interface InitOutput { readonly gameengine_getCamera: (a: number) => [number, number]; readonly gameengine_getOrLoadTextureByPath: (a: number, b: number, c: number) => [number, number, number]; readonly gameengine_getTextureIdByPath: (a: number, b: number, c: number) => number; + readonly gameengine_getTextureLoadingCount: (a: number) => number; + readonly gameengine_getTextureState: (a: number, b: number) => [number, number]; readonly gameengine_getViewportCamera: (a: number, b: number, c: number) => [number, number]; readonly gameengine_getViewportIds: (a: number) => [number, number]; readonly gameengine_hasMaterial: (a: number, b: number) => number; @@ -436,6 +463,7 @@ export interface InitOutput { readonly gameengine_height: (a: number) => number; readonly gameengine_isEditorMode: (a: number) => number; readonly gameengine_isKeyDown: (a: number, b: number, c: number) => number; + readonly gameengine_isTextureReady: (a: number, b: number) => number; readonly gameengine_loadTexture: (a: number, b: number, c: number, d: number) => [number, number]; readonly gameengine_loadTextureByPath: (a: number, b: number, c: number) => [number, number, number]; readonly gameengine_new: (a: number, b: number) => [number, number, number]; diff --git a/packages/engine/src/core/engine.rs b/packages/engine/src/core/engine.rs index ae83c4cd..2eedff63 100644 --- a/packages/engine/src/core/engine.rs +++ b/packages/engine/src/core/engine.rs @@ -197,14 +197,6 @@ impl Engine { colors: &[u32], material_ids: &[u32], ) -> Result<()> { - // Debug: log once - use std::sync::atomic::{AtomicBool, Ordering}; - static LOGGED: AtomicBool = AtomicBool::new(false); - if !LOGGED.swap(true, Ordering::Relaxed) { - let sprite_count = texture_ids.len(); - log::info!("Engine submit_sprite_batch: {} sprites, texture_ids: {:?}", sprite_count, texture_ids); - } - self.renderer.submit_batch( transforms, texture_ids, @@ -382,6 +374,24 @@ impl Engine { self.texture_manager.clear_all(); } + /// 获取纹理加载状态 + /// Get texture loading state + pub fn get_texture_state(&self, id: u32) -> crate::renderer::texture::TextureState { + self.texture_manager.get_texture_state(id) + } + + /// 检查纹理是否已就绪 + /// Check if texture is ready to use + pub fn is_texture_ready(&self, id: u32) -> bool { + self.texture_manager.is_texture_ready(id) + } + + /// 获取正在加载中的纹理数量 + /// Get the number of textures currently loading + pub fn get_texture_loading_count(&self) -> u32 { + self.texture_manager.get_loading_count() + } + /// Check if a key is currently pressed. /// 检查某个键是否当前被按下。 pub fn is_key_down(&self, key_code: &str) -> bool { diff --git a/packages/engine/src/lib.rs b/packages/engine/src/lib.rs index b59e9d6b..ea50d589 100644 --- a/packages/engine/src/lib.rs +++ b/packages/engine/src/lib.rs @@ -224,6 +224,42 @@ impl GameEngine { .map_err(|e| JsValue::from_str(&e.to_string())) } + /// 获取纹理加载状态 + /// Get texture loading state + /// + /// # Arguments | 参数 + /// * `id` - Texture ID | 纹理ID + /// + /// # Returns | 返回 + /// State string: "loading", "ready", or "failed:reason" + /// 状态字符串:"loading"、"ready" 或 "failed:原因" + #[wasm_bindgen(js_name = getTextureState)] + pub fn get_texture_state(&self, id: u32) -> String { + use crate::renderer::texture::TextureState; + match self.engine.get_texture_state(id) { + TextureState::Loading => "loading".to_string(), + TextureState::Ready => "ready".to_string(), + TextureState::Failed(reason) => format!("failed:{}", reason), + } + } + + /// 检查纹理是否已就绪 + /// Check if texture is ready to use + /// + /// # Arguments | 参数 + /// * `id` - Texture ID | 纹理ID + #[wasm_bindgen(js_name = isTextureReady)] + pub fn is_texture_ready(&self, id: u32) -> bool { + self.engine.is_texture_ready(id) + } + + /// 获取正在加载中的纹理数量 + /// Get the number of textures currently loading + #[wasm_bindgen(js_name = getTextureLoadingCount)] + pub fn get_texture_loading_count(&self) -> u32 { + self.engine.get_texture_loading_count() + } + /// Check if a key is currently pressed. /// 检查某个键是否当前被按下。 /// diff --git a/packages/engine/src/renderer/texture/mod.rs b/packages/engine/src/renderer/texture/mod.rs index e3be5892..9298c5cb 100644 --- a/packages/engine/src/renderer/texture/mod.rs +++ b/packages/engine/src/renderer/texture/mod.rs @@ -5,4 +5,4 @@ mod texture; mod texture_manager; pub use texture::Texture; -pub use texture_manager::TextureManager; +pub use texture_manager::{TextureManager, TextureState}; diff --git a/packages/engine/src/renderer/texture/texture_manager.rs b/packages/engine/src/renderer/texture/texture_manager.rs index d41846fc..60524b49 100644 --- a/packages/engine/src/renderer/texture/texture_manager.rs +++ b/packages/engine/src/renderer/texture/texture_manager.rs @@ -1,7 +1,9 @@ //! Texture loading and management. //! 纹理加载和管理。 +use std::cell::RefCell; use std::collections::HashMap; +use std::rc::Rc; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; use web_sys::{HtmlImageElement, WebGl2RenderingContext, WebGlTexture}; @@ -9,6 +11,21 @@ use web_sys::{HtmlImageElement, WebGl2RenderingContext, WebGlTexture}; use crate::core::error::{EngineError, Result}; use super::Texture; +/// 纹理加载状态 +/// Texture loading state +#[derive(Debug, Clone, PartialEq)] +pub enum TextureState { + /// 正在加载中 + /// Loading in progress + Loading, + /// 加载完成,可以使用 + /// Loaded and ready to use + Ready, + /// 加载失败 + /// Load failed + Failed(String), +} + /// Texture manager for loading and caching textures. /// 用于加载和缓存纹理的纹理管理器。 pub struct TextureManager { @@ -31,6 +48,10 @@ pub struct TextureManager { /// Default white texture for untextured rendering. /// 用于无纹理渲染的默认白色纹理。 default_texture: Option, + + /// 纹理加载状态(使用 Rc> 以便闭包可以修改) + /// Texture loading states (using Rc> so closures can modify) + texture_states: Rc>>, } impl TextureManager { @@ -43,6 +64,7 @@ impl TextureManager { path_to_id: HashMap::new(), next_id: 1, // Start from 1, 0 is reserved for default default_texture: None, + texture_states: Rc::new(RefCell::new(HashMap::new())), }; // Create default white texture | 创建默认白色纹理 @@ -90,17 +112,22 @@ impl TextureManager { /// 从URL加载纹理。 /// /// Note: This is an async operation. The texture will be available - /// after the image loads. - /// 注意:这是一个异步操作。纹理在图片加载后可用。 + /// after the image loads. Use `get_texture_state` to check loading status. + /// 注意:这是一个异步操作。纹理在图片加载后可用。使用 `get_texture_state` 检查加载状态。 pub fn load_texture(&mut self, id: u32, url: &str) -> Result<()> { + // 设置初始状态为 Loading | Set initial state to Loading + self.texture_states.borrow_mut().insert(id, TextureState::Loading); + // Create placeholder texture | 创建占位纹理 let texture = self.gl .create_texture() .ok_or_else(|| EngineError::TextureLoadFailed("Failed to create texture".into()))?; - // Set up temporary 1x1 texture | 设置临时1x1纹理 + // Set up temporary 1x1 transparent texture | 设置临时1x1透明纹理 + // 使用透明而非灰色,这样未加载完成时不会显示奇怪的颜色 + // Use transparent instead of gray, so incomplete textures don't show strange colors self.gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(&texture)); - let placeholder: [u8; 4] = [128, 128, 128, 255]; + let placeholder: [u8; 4] = [0, 0, 0, 0]; let _ = self.gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array( WebGl2RenderingContext::TEXTURE_2D, 0, @@ -119,6 +146,10 @@ impl TextureManager { // Store texture with placeholder size | 存储带占位符尺寸的纹理 self.textures.insert(id, Texture::new(texture, 1, 1)); + // Clone state map for closures | 克隆状态映射用于闭包 + let states_for_onload = Rc::clone(&self.texture_states); + let states_for_onerror = Rc::clone(&self.texture_states); + // Load actual image asynchronously | 异步加载实际图片 let gl = self.gl.clone(); @@ -130,6 +161,7 @@ impl TextureManager { // Clone image for use in closure | 克隆图片用于闭包 let image_clone = image.clone(); + let texture_id = id; // Set up load callback | 设置加载回调 let onload = Closure::wrap(Box::new(move || { @@ -146,7 +178,9 @@ impl TextureManager { ); if let Err(e) = result { - log::error!("Failed to upload texture: {:?} | 纹理上传失败: {:?}", e, e); + log::error!("Failed to upload texture {}: {:?} | 纹理 {} 上传失败: {:?}", texture_id, e, texture_id, e); + states_for_onload.borrow_mut().insert(texture_id, TextureState::Failed(format!("{:?}", e))); + return; } // Set texture parameters | 设置纹理参数 @@ -171,10 +205,22 @@ impl TextureManager { WebGl2RenderingContext::LINEAR as i32, ); + // 标记为就绪 | Mark as ready + states_for_onload.borrow_mut().insert(texture_id, TextureState::Ready); + + }) as Box); + + // Set up error callback | 设置错误回调 + let url_for_error = url.to_string(); + let onerror = Closure::wrap(Box::new(move || { + let error_msg = format!("Failed to load image: {}", url_for_error); + states_for_onerror.borrow_mut().insert(texture_id, TextureState::Failed(error_msg)); }) as Box); image.set_onload(Some(onload.as_ref().unchecked_ref())); + image.set_onerror(Some(onerror.as_ref().unchecked_ref())); onload.forget(); // Prevent closure from being dropped | 防止闭包被销毁 + onerror.forget(); image.set_src(url); @@ -223,6 +269,56 @@ impl TextureManager { self.textures.contains_key(&id) } + /// 获取纹理加载状态 + /// Get texture loading state + /// + /// 返回纹理的当前加载状态:Loading、Ready 或 Failed。 + /// Returns the current loading state of the texture: Loading, Ready, or Failed. + #[inline] + pub fn get_texture_state(&self, id: u32) -> TextureState { + // ID 0 是默认纹理,始终就绪 + // ID 0 is default texture, always ready + if id == 0 { + return TextureState::Ready; + } + + self.texture_states + .borrow() + .get(&id) + .cloned() + .unwrap_or(TextureState::Failed("Texture not found".to_string())) + } + + /// 检查纹理是否已就绪可用 + /// Check if texture is ready to use + /// + /// 这是 `get_texture_state() == TextureState::Ready` 的便捷方法。 + /// This is a convenience method for `get_texture_state() == TextureState::Ready`. + #[inline] + pub fn is_texture_ready(&self, id: u32) -> bool { + // ID 0 是默认纹理,始终就绪 + // ID 0 is default texture, always ready + if id == 0 { + return true; + } + + matches!( + self.texture_states.borrow().get(&id), + Some(TextureState::Ready) + ) + } + + /// 获取正在加载中的纹理数量 + /// Get the number of textures currently loading + #[inline] + pub fn get_loading_count(&self) -> u32 { + self.texture_states + .borrow() + .values() + .filter(|s| matches!(s, TextureState::Loading)) + .count() as u32 + } + /// Remove texture. /// 移除纹理。 pub fn remove_texture(&mut self, id: u32) { @@ -231,6 +327,8 @@ impl TextureManager { } // Also remove from path mapping | 同时从路径映射中移除 self.path_to_id.retain(|_, &mut v| v != id); + // Remove state | 移除状态 + self.texture_states.borrow_mut().remove(&id); } /// Load texture by path, returning texture ID. @@ -308,6 +406,9 @@ impl TextureManager { // Clear path mapping | 清除路径映射 self.path_to_id.clear(); + // Clear texture states | 清除纹理状态 + self.texture_states.borrow_mut().clear(); + // Reset ID counter (1 is reserved for first texture, 0 for default) // 重置ID计数器(1保留给第一个纹理,0给默认纹理) self.next_id = 1;