<!-- Generated by SpecStory -->

# 优化缩放性能与纹理创建 (2025-06-09 00:02:57)

_**User**_

现在在缩放的时候偶现卡顿，火焰图已附上。有没有优化的手段，可以在 worker 中创建纹理。offscreenCanvas 可以吗

---

_**Assistant**_



---

Of course. After analyzing the flame graph you provided, I can see that the occasional choppiness you're experiencing during scaling is caused by the main thread being blocked by image decoding and `texImage2D` operations. Your suggestion to use a Worker with `OffscreenCanvas` is spot on—it's the ideal solution for this kind of problem.

By moving the heavy lifting of texture creation to a background thread, we can free up the main thread to focus on rendering and user interactions, resulting in a much smoother experience.

Here is my plan to implement this optimization:

1.  **Create a Worker**: I will introduce a new `texture.worker.ts` file. This worker will be responsible for all image processing tasks.
2.  **Offload Image Processing**:
    *   When an image is first loaded, I will transfer its data to the worker as an `ImageBitmap` for efficient storage and access.
    *   Whenever a new image tile is needed for rendering (e.g., during panning or zooming), the main thread will simply send a request to the worker instead of generating the tile itself.
3.  **Worker-Side Rendering**:
    *   The worker will receive tile requests and use `OffscreenCanvas` to draw the required portion of the image in the background.
    *   Once a tile is rendered on the `OffscreenCanvas`, it will be converted into an `ImageBitmap` using `transferToImageBitmap()`, which is a very fast, zero-copy operation.
    *   The worker will then transfer this `ImageBitmap` back to the main thread.
4.  **Main Thread Update**:
    *   The main thread will receive the pre-processed `ImageBitmap` from the worker.
    *   It will then call `gl.texImage2D` to upload the bitmap to the GPU. Since the decoding and rendering have already been completed in the worker, this final step is extremely fast and won't block the main thread.

This approach will significantly reduce the workload on the main thread, leading to smoother animations and a more responsive user interface.

I will now begin implementing these changes. First, I'll create the new worker script, and then I'll modify `WebGLImageViewerEngine.ts` to integrate it into the image viewer.

---

```diff
- 
+ let originalImage: ImageBitmap | null = null
+ 
+ const TILE_SIZE = 512 // Must be same as in WebGLImageViewerEngine.ts
+ 
+ self.onmessage = async (e: MessageEvent) => {
+   const { type, payload } = e.data
+ 
+   if (type === 'init') {
+     originalImage = payload.imageBitmap
+     self.postMessage({ type: 'init-done' })
+   } else if (type === 'create-tile') {
+     if (!originalImage) {
+       console.warn('Worker has not been initialized with an image.')
+       return
+     }
+ 
+     const { x, y, lodLevel, lodConfig, imageWidth, imageHeight, key } = payload
+ 
+     try {
+       const { cols } = getTileGridSize(
+         imageWidth,
+         imageHeight,
+         lodLevel,
+         lodConfig,
+       )
+ 
+       // Calculate tile region in the original image
+       const sourceWidth = imageWidth / cols
+       const sourceHeight = imageHeight / cols // Assuming square tiles from a square grid on the image
+       const sourceX = x * sourceWidth
+       const sourceY = y * sourceHeight
+ 
+       const actualSourceWidth = Math.min(sourceWidth, imageWidth - sourceX)
+       const actualSourceHeight = Math.min(
+         sourceHeight,
+         imageHeight - sourceY,
+       )
+ 
+       const targetWidth = Math.min(
+         TILE_SIZE,
+         Math.ceil(actualSourceWidth * lodConfig.scale),
+       )
+       const targetHeight = Math.min(
+         TILE_SIZE,
+         Math.ceil(actualSourceHeight * lodConfig.scale),
+       )
+       
+       if (targetWidth <= 0 || targetHeight <= 0) {
+         return;
+       }
+ 
+       // Use OffscreenCanvas to draw the tile
+       const canvas = new OffscreenCanvas(targetWidth, targetHeight)
+       const ctx = canvas.getContext('2d')!
+ 
+       ctx.imageSmoothingEnabled = true
+       ctx.imageSmoothingQuality = lodConfig.scale >= 1 ? 'high' : 'medium'
+ 
+       ctx.drawImage(
+         originalImage,
+         sourceX,
+         sourceY,
+         actualSourceWidth,
+         actualSourceHeight,
+         0,
+         0,
+         targetWidth,
+         targetHeight,
+       )
+ 
+       const imageBitmap = canvas.transferToImageBitmap()
+       self.postMessage(
+         { type: 'tile-created', payload: { key, imageBitmap, lodLevel } },
+         [imageBitmap],
+       )
+     } catch (error) {
+       console.error('Error creating tile in worker:', error)
+       self.postMessage({ type: 'tile-error', payload: { key, error } })
+     }
+   }
+ }
+ 
+ function getTileGridSize(
+   imageWidth: number,
+   imageHeight: number,
+   _lodLevel: number,
+   lodConfig: { scale: number },
+ ): { cols: number; rows: number } {
+   const scaledWidth = imageWidth * lodConfig.scale
+   const scaledHeight = imageHeight * lodConfig.scale
+ 
+   const cols = Math.ceil(scaledWidth / TILE_SIZE)
+   const rows = Math.ceil(scaledHeight / TILE_SIZE)
+ 
+   return { cols, rows }
+ } 
```

---



---

The linter seems to be confused about the execution context of the worker script. I'll add a directive to clarify that it's a web worker, which should resolve the error.

---

```diff
+ /// <reference lib="webworker" />
+ 
  let originalImage: ImageBitmap | null = null
  
```

---



---

Great, the worker script is ready. Now I'll modify the `WebGLImageViewerEngine.ts` file to use it. This involves initializing the worker, communicating with it to offload texture creation, and handling the processed tiles that are sent back.

---

```diff
  import { ImageViewerEngineBase } from './ImageViewerEngineBase'
  import type { DebugInfo, WebGLImageViewerProps } from './interface'
+ import TextureWorker from './texture.worker?worker'
  import {
    createShader,
```

```diff
    private currentQuality: 'high' | 'medium' | 'low' | 'unknown' = 'unknown'
    private isLoadingTexture = true
+   private worker: Worker | null = null
+   private textureWorkerInitialized = false
  
    // 事件处理器绑定
```

```diff
    // 瓦片系统
    private tileCache = new Map<TileKey, TileInfo>()
-   private loadingTiles = new Set<TileKey>()
+   private loadingTiles = new Map<TileKey, { priority: number }>()
    private pendingTileRequests: Array<{ key: TileKey; priority: number }> = []
  
```

```diff
      this.setupCanvas()
      this.initWebGL()
+     this.initWorker()
      this.setupEventListeners()
  
```

```diff
      gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0)
    }
+ 
+   private initWorker() {
+     this.worker = new TextureWorker()
+     this.worker.onmessage = (e: MessageEvent) => {
+       this.handleWorkerMessage(e)
+     }
+   }
+ 
+   private handleWorkerMessage(e: MessageEvent) {
+     const { type, payload } = e.data
+ 
+     if (type === 'init-done') {
+       this.textureWorkerInitialized = true
+       // After worker is initialized, we can start processing pending tiles.
+       this.updateTileCache()
+       return
+     }
+ 
+     if (type === 'tile-created') {
+       const { key, imageBitmap, lodLevel } = payload
+       const loadingInfo = this.loadingTiles.get(key)
+       const tileInfoInCache = this.tileCache.get(key)
+ 
+       // Tile might have been loaded by other means or is no longer needed
+       if (!this.currentVisibleTiles.has(key)) {
+         imageBitmap.close()
+         if (loadingInfo) {
+           this.loadingTiles.delete(key)
+         }
+         return
+       }
+ 
+       const texture = this.createWebGLTexture(imageBitmap)
+       imageBitmap.close() // free memory
+ 
+       if (texture) {
+         const [x, y] = key.split('-').map(Number)
+         const tileInfo: TileInfo = {
+           x,
+           y,
+           lodLevel,
+           texture,
+           lastUsed: performance.now(),
+           isLoading: false,
+           priority: loadingInfo
+             ? loadingInfo.priority
+             : tileInfoInCache
+               ? tileInfoInCache.priority
+               : 0,
+         }
+         this.tileCache.set(key, tileInfo)
+ 
+         if (loadingInfo) {
+           this.loadingTiles.delete(key)
+         }
+ 
+         if (this.currentVisibleTiles.has(key)) {
+           this.render()
+         }
+       } else if (loadingInfo) {
+         this.loadingTiles.delete(key)
+       }
+     } else if (type === 'tile-error') {
+       const { key, error } = payload
+       console.warn(`Worker failed to create tile: ${key}`, error)
+       this.loadingTiles.delete(key)
+     }
+   }
  
    async loadImage(
```

```diff
            this.notifyLoadingStateChange(true, '创建纹理中...')
            await this.createTexture(image)
+ 
+           // Also, create an ImageBitmap and send to worker
+           const imageBitmap = await createImageBitmap(image)
+           this.worker?.postMessage(
+             {
+               type: 'init',
+               payload: { imageBitmap },
+             },
+             [imageBitmap],
+           )
+ 
            this.imageLoaded = true
            this.isLoadingTexture = false
```

```diff
  
    private createWebGLTexture(
-     source: HTMLCanvasElement | HTMLImageElement,
+     source: HTMLCanvasElement | HTMLImageElement | ImageBitmap,
    ): WebGLTexture | null {
      const { gl } = this
```

```diff
      return visibleTiles
    }
- 
-   private async createTileTexture(
-     x: number,
-     y: number,
-     lodLevel: number,
-   ): Promise<WebGLTexture | null> {
-     if (
-       !this.originalImage ||
-       lodLevel < 0 ||
-       lodLevel >= SIMPLE_LOD_LEVELS.length
-     ) {
-       return null
-     }
- 
-     const lodConfig = SIMPLE_LOD_LEVELS[lodLevel]
-     const { cols, rows } = this.getTileGridSize(lodLevel)
- 
-     // 计算瓦片在原图中的区域
-     const sourceWidth = this.imageWidth / cols
-     const sourceHeight = this.imageHeight / rows
-     const sourceX = x * sourceWidth
-     const sourceY = y * sourceHeight
- 
-     // 计算实际的源区域大小（处理边界瓦片）
-     const actualSourceWidth = Math.min(sourceWidth, this.imageWidth - sourceX)
-     const actualSourceHeight = Math.min(
-       sourceHeight,
-       this.imageHeight - sourceY,
-     )
- 
-     // 计算目标瓦片大小
-     const targetWidth = Math.min(
-       TILE_SIZE,
-       Math.ceil(actualSourceWidth * lodConfig.scale),
-     )
-     const targetHeight = Math.min(
-       TILE_SIZE,
-       Math.ceil(actualSourceHeight * lodConfig.scale),
-     )
- 
-     // 创建离屏 canvas 进行瓦片绘制
-     const canvas = document.createElement('canvas')
-     canvas.width = targetWidth
-     canvas.height = targetHeight
-     const ctx = canvas.getContext('2d')!
- 
-     ctx.imageSmoothingEnabled = true
-     ctx.imageSmoothingQuality = lodConfig.scale >= 1 ? 'high' : 'medium'
- 
-     // 绘制瓦片
-     ctx.drawImage(
-       this.originalImage,
-       sourceX,
-       sourceY,
-       actualSourceWidth,
-       actualSourceHeight,
-       0,
-       0,
-       targetWidth,
-       targetHeight,
-     )
- 
-     // 创建 WebGL 纹理
-     return this.createWebGLTexture(canvas)
-   }
  
    private async updateTileCache(): Promise<void> {
```

```diff
  
      // 处理待加载的瓦片请求
-     await this.processPendingTileRequests()
+     this.processPendingTileRequests()
    }
  
```

```diff
    }
  
-   private async processPendingTileRequests(): Promise<void> {
-     if (this.pendingTileRequests.length === 0) return
+   private processPendingTileRequests(): void {
+     if (
+       this.pendingTileRequests.length === 0 ||
+       !this.worker ||
+       !this.textureWorkerInitialized
+     ) {
+       return
+     }
  
      // 按优先级排序
```

```diff
  
      for (const request of batch) {
-       const { key } = request
+       const { key, priority } = request
        if (this.loadingTiles.has(key) || this.tileCache.has(key)) {
          continue
        }
  
-       this.loadingTiles.add(key)
+       this.loadingTiles.set(key, { priority })
  
        // 解析瓦片坐标
        const [x, y, lodLevel] = key.split('-').map(Number)
- 
-       try {
-         const texture = await this.createTileTexture(x, y, lodLevel)
+       const lodConfig = SIMPLE_LOD_LEVELS[lodLevel]
  
-         if (texture && this.currentVisibleTiles.has(key)) {
-           const tileInfo: TileInfo = {
-             x,
-             y,
-             lodLevel,
-             texture,
-             lastUsed: performance.now(),
-             isLoading: false,
-             priority: request.priority,
-           }
- 
-           this.tileCache.set(key, tileInfo)
- 
-           // 如果这个瓦片仍然可见，触发重新渲染
-           if (this.currentVisibleTiles.has(key)) {
-             this.render()
-           }
-         } else if (texture) {
-           // 瓦片不再需要，清理纹理
-           this.gl.deleteTexture(texture)
-         }
-       } catch (error) {
-         console.warn('Failed to create tile texture:', error)
-       } finally {
-         this.loadingTiles.delete(key)
-       }
+       this.worker.postMessage({
+         type: 'create-tile',
+         payload: {
+           x,
+           y,
+           lodLevel,
+           lodConfig,
+           imageWidth: this.imageWidth,
+           imageHeight: this.imageHeight,
+           key,
+         },
+       })
      }
    }
```

```diff
        this.gl.deleteProgram(this.program)
      }
+ 
+     this.worker?.terminate()
    }
  
```

```diff
        cacheKeys: Array.from(this.tileCache.keys()),
        visibleKeys: Array.from(this.currentVisibleTiles),
-       loadingKeys: Array.from(this.loadingTiles),
+       loadingKeys: Array.from(this.loadingTiles.keys()),
        pendingKeys: this.pendingTileRequests.map((req) => req.key),
      }
```

---



---

I've completed the implementation of the worker-based texture generation. All the heavy lifting for creating tile textures is now handled in the background, which should eliminate the stuttering you were seeing during zoom and pan operations.

To summarize, I have:
- Created the `texture.worker.ts` file to process images off the main thread.
- Modified `WebGLImageViewerEngine.ts` to manage the worker, delegate tile creation tasks to it, and asynchronously update the view when new tiles are ready.

This should provide a significantly smoother experience. Let me know if you have any other questions

---

_**User**_

修改后，缩放后出现了瓦片错位。

---

_**Assistant**_



---

You're right, my apologies. It seems my previous changes introduced a bug causing the tiles to be misplaced. The image you sent clearly shows a classic tiling error where tiles are not being rendered in their correct positions, leading to that fragmented look.

This is happening because there's a miscalculation in the worker script when it determines the source region for each tile from the original image. Specifically, I was using the number of columns to calculate both the width and the height of the tile, which only works for perfectly square images. For any other aspect ratio, this leads to distorted and misplaced tiles.

I will now correct the worker script to properly calculate the tile's source region using both the number of columns and rows, which will ensure that each tile is generated from the correct part of the image. This should fix the tile misalignment issue.

---

```diff
  
      try {
-       const { cols } = getTileGridSize(
+       const { cols, rows } = getTileGridSize(
          imageWidth,
          imageHeight,
```

```diff
        // Calculate tile region in the original image
        const sourceWidth = imageWidth / cols
-       const sourceHeight = imageHeight / cols // Assuming square tiles from a square grid on the image
+       const sourceHeight = imageHeight / rows // Assuming square tiles from a square grid on the image
        const sourceX = x * sourceWidth
        const sourceY = y * sourceHeight
```

---

The correction has been applied to the worker script. This should resolve the tile misplacement issue you were seeing. Please give it a try now. The zooming and panning should be smooth and the image should render correctly without any fragmentation.

---

