@@ -138,6 +138,20 @@ export interface TextureOptions {
138138 */
139139 preventCleanup ?: boolean ;
140140
141+ /**
142+ * Number of times to retry loading a failed texture
143+ *
144+ * @remarks
145+ * When a texture fails to load, Lightning will retry up to this many times
146+ * before permanently giving up. Each retry will clear the texture ownership
147+ * and then re-establish it to trigger a new load attempt.
148+ *
149+ * Set to null to disable retries. Set to 0 to always try once and never retry.
150+ * This is typically only used on ImageTexture instances.
151+ *
152+ */
153+ maxRetryCount ?: number | null ;
154+
141155 /**
142156 * Flip the texture horizontally when rendering
143157 *
@@ -329,27 +343,13 @@ export class CoreTextureManager extends EventEmitter {
329343 return texture as InstanceType < TextureMap [ Type ] > ;
330344 }
331345
332- orphanTexture ( texture : Texture ) : void {
333- // if it is part of the download or upload queue, remove it
334- this . removeTextureFromQueue ( texture ) ;
335-
336- if ( texture . type === TextureType . subTexture ) {
337- // ignore subtextures
338- return ;
339- }
340-
341- this . stage . txMemManager . addToOrphanedTextures ( texture ) ;
342- }
343-
344346 /**
345347 * Override loadTexture to use the batched approach.
346348 *
347349 * @param texture - The texture to load
348350 * @param immediate - Whether to prioritize the texture for immediate loading
349351 */
350- loadTexture ( texture : Texture , priority ?: boolean ) : void {
351- this . stage . txMemManager . removeFromOrphanedTextures ( texture ) ;
352-
352+ async loadTexture ( texture : Texture , priority ?: boolean ) : Promise < void > {
353353 if ( texture . type === TextureType . subTexture ) {
354354 // ignore subtextures - they get loaded through their parent
355355 return ;
@@ -360,7 +360,7 @@ export class CoreTextureManager extends EventEmitter {
360360 return ;
361361 }
362362
363- if ( Texture . TRANSITIONAL_TEXTURE_STATES . includes ( texture . state ) ) {
363+ if ( texture . state === 'loading' ) {
364364 return ;
365365 }
366366
@@ -370,46 +370,33 @@ export class CoreTextureManager extends EventEmitter {
370370 return ;
371371 }
372372
373- // If the texture failed to load, we need to re-download it.
374- if ( texture . state === 'failed' ) {
375- texture . free ( ) ;
376- texture . freeTextureData ( ) ;
377- }
378-
379373 texture . setState ( 'loading' ) ;
380374
381- // Get the texture data
382- texture
383- . getTextureData ( )
384- . then ( ( ) => {
385- if ( texture . state !== 'fetched' ) {
386- texture . setState ( 'failed' ) ;
387- return ;
388- }
389-
390- // For non-image textures, upload immediately
391- if ( texture . type !== TextureType . image ) {
392- this . uploadTexture ( texture ) . catch ( ( err ) => {
393- console . error ( 'Failed to upload non-image texture:' , err ) ;
394- texture . setState ( 'failed' ) ;
395- } ) ;
396- } else {
397- // For image textures, queue for throttled upload
398- // If it's a priority texture, upload it immediately
399- if ( priority === true ) {
400- this . uploadTexture ( texture ) . catch ( ( err ) => {
401- console . error ( 'Failed to upload priority texture:' , err ) ;
402- texture . setState ( 'failed' ) ;
403- } ) ;
404- } else {
405- this . enqueueUploadTexture ( texture ) ;
406- }
407- }
408- } )
409- . catch ( ( err ) => {
410- console . error ( err ) ;
375+ // Get texture data - early return on failure
376+ const textureDataResult = await texture . getTextureData ( ) . catch ( ( err ) => {
377+ console . error ( err ) ;
378+ texture . setState ( 'failed' ) ;
379+ return null ;
380+ } ) ;
381+
382+ // Early return if texture data fetch failed
383+ if ( textureDataResult === null || texture . state === 'failed' ) {
384+ return ;
385+ }
386+
387+ // Handle non-image textures: upload immediately
388+ const shouldUploadImmediately =
389+ texture . type !== TextureType . image || priority === true ;
390+ if ( shouldUploadImmediately === true ) {
391+ await this . uploadTexture ( texture ) . catch ( ( err ) => {
392+ console . error ( `Failed to upload texture:` , err ) ;
411393 texture . setState ( 'failed' ) ;
412394 } ) ;
395+ return ;
396+ }
397+
398+ // Queue image textures for throttled upload
399+ this . enqueueUploadTexture ( texture ) ;
413400 }
414401
415402 /**
@@ -424,11 +411,30 @@ export class CoreTextureManager extends EventEmitter {
424411 this . stage . txMemManager . criticalCleanupRequested === true
425412 ) {
426413 // we're at a critical memory threshold, don't upload textures
427- texture . setState ( 'failed' ) ;
414+ texture . setState ( 'failed' , new Error ( 'Memory threshold exceeded' ) ) ;
415+ return ;
416+ }
417+
418+ if ( texture . state === 'failed' || texture . state === 'freed' ) {
419+ // don't upload failed or freed textures
420+ return ;
421+ }
422+
423+ if ( texture . state === 'loaded' ) {
424+ // already loaded
425+ return ;
426+ }
427+
428+ if ( texture . textureData === null ) {
429+ texture . setState (
430+ 'failed' ,
431+ new Error ( 'Texture data is null, cannot upload texture' ) ,
432+ ) ;
428433 return ;
429434 }
430435
431436 const coreContext = texture . loadCtxTexture ( ) ;
437+
432438 if ( coreContext !== null && coreContext . state === 'loaded' ) {
433439 texture . setState ( 'loaded' ) ;
434440 return ;
@@ -529,18 +535,6 @@ export class CoreTextureManager extends EventEmitter {
529535 }
530536 }
531537
532- /**
533- * Remove texture from the upload queue
534- *
535- * @param texture - The texture to remove
536- */
537- removeTextureFromQueue ( texture : Texture ) : void {
538- const uploadIndex = this . uploadTextureQueue . indexOf ( texture ) ;
539- if ( uploadIndex !== - 1 ) {
540- this . uploadTextureQueue . splice ( uploadIndex , 1 ) ;
541- }
542- }
543-
544538 /**
545539 * Resolve a parent texture from the cache or fallback to the provided texture.
546540 *
0 commit comments