From 03d0eddecf0971bd6f628cfbe6a0180813567f8a Mon Sep 17 00:00:00 2001 From: MiraclePatrickLumowa Date: Fri, 22 Aug 2025 20:52:42 +0800 Subject: [PATCH] Fixed batch skipping + Optimized Overlay Processing --- Auto-Image.js | 329 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 216 insertions(+), 113 deletions(-) diff --git a/Auto-Image.js b/Auto-Image.js index 9ff71ba..1c8527c 100644 --- a/Auto-Image.js +++ b/Auto-Image.js @@ -209,7 +209,7 @@ } } - // BILINGUAL TEXT STRINGS + // BILINGUAL TEXT STRINGS:) const TEXT = { en: { title: "WPlace Auto-Image", @@ -992,7 +992,7 @@ // Placeholder for the resize preview update function let _updateResizePreview = () => { }; - // --- OVERLAY UPDATE: New OverlayManager class to handle all overlay logic --- + // --- OVERLAY UPDATE: Optimized OverlayManager class with performance improvements --- class OverlayManager { constructor() { this.isEnabled = false; @@ -1000,6 +1000,9 @@ this.imageBitmap = null; this.chunkedTiles = new Map(); // Map<"tileX,tileY", ImageBitmap> this.tileSize = 1000; + this.processPromise = null; // Track ongoing processing + this.lastProcessedHash = null; // Cache invalidation + this.workerPool = null; // Web worker pool for heavy processing } toggle() { @@ -1014,10 +1017,15 @@ this.disable(); this.imageBitmap = null; this.chunkedTiles.clear(); + this.lastProcessedHash = null; + if (this.processPromise) { + this.processPromise = null; + } } async setImage(imageBitmap) { this.imageBitmap = imageBitmap; + this.lastProcessedHash = null; // Invalidate cache if (this.imageBitmap && this.startCoords) { await this.processImageIntoChunks(); } @@ -1027,19 +1035,55 @@ if (!startPosition || !region) { this.startCoords = null; this.chunkedTiles.clear(); + this.lastProcessedHash = null; return; } this.startCoords = { region, pixel: startPosition }; + this.lastProcessedHash = null; // Invalidate cache if (this.imageBitmap) { await this.processImageIntoChunks(); } } - // --- OVERLAY UPDATE: Simplified chunking logic for solid, semi-transparent overlay --- + // Generate hash for cache invalidation + _generateProcessHash() { + if (!this.imageBitmap || !this.startCoords) return null; + const { width, height } = this.imageBitmap; + const { x: px, y: py } = this.startCoords.pixel; + const { x: rx, y: ry } = this.startCoords.region; + return `${width}x${height}_${px},${py}_${rx},${ry}_${state.blueMarbleEnabled}_${state.overlayOpacity}`; + } + + // --- OVERLAY UPDATE: Optimized chunking with caching and batch processing --- async processImageIntoChunks() { if (!this.imageBitmap || !this.startCoords) return; + // Check if we're already processing to avoid duplicate work + if (this.processPromise) { + return this.processPromise; + } + + // Check cache validity + const currentHash = this._generateProcessHash(); + if (this.lastProcessedHash === currentHash && this.chunkedTiles.size > 0) { + console.log(`📦 Using cached overlay chunks (${this.chunkedTiles.size} tiles)`); + return; + } + + // Start processing + this.processPromise = this._doProcessImageIntoChunks(); + try { + await this.processPromise; + this.lastProcessedHash = currentHash; + } finally { + this.processPromise = null; + } + } + + async _doProcessImageIntoChunks() { + const startTime = performance.now(); this.chunkedTiles.clear(); + const { width: imageWidth, height: imageHeight } = this.imageBitmap; const { x: startPixelX, y: startPixelY } = this.startCoords.pixel; const { x: startRegionX, y: startRegionY } = this.startCoords.region; @@ -1052,63 +1096,89 @@ const endTileX = startRegionX + Math.floor(endPixelX / this.tileSize); const endTileY = startRegionY + Math.floor(endPixelY / this.tileSize); + const totalTiles = (endTileX - startTileX + 1) * (endTileY - startTileY + 1); + console.log(`🔄 Processing ${totalTiles} overlay tiles...`); + + // Process tiles in batches to avoid blocking the main thread + const batchSize = 4; // Process 4 tiles at a time + const tilesToProcess = []; + for (let ty = startTileY; ty <= endTileY; ty++) { for (let tx = startTileX; tx <= endTileX; tx++) { - const tileKey = `${tx},${ty}`; - - // Calculate the portion of the image that overlaps with this tile - const imgStartX = (tx - startRegionX) * this.tileSize - startPixelX; - const imgStartY = (ty - startRegionY) * this.tileSize - startPixelY; - - // Crop coordinates within the source image - const sX = Math.max(0, imgStartX); - const sY = Math.max(0, imgStartY); - const sW = Math.min(imageWidth - sX, this.tileSize - (sX - imgStartX)); - const sH = Math.min(imageHeight - sY, this.tileSize - (sY - imgStartY)); - - if (sW <= 0 || sH <= 0) continue; - - // Destination coordinates on the new chunk canvas - const dX = Math.max(0, -imgStartX); - const dY = Math.max(0, -imgStartY); - - const chunkCanvas = new OffscreenCanvas(this.tileSize, this.tileSize); - const chunkCtx = chunkCanvas.getContext('2d'); - chunkCtx.imageSmoothingEnabled = false; - - chunkCtx.drawImage(this.imageBitmap, sX, sY, sW, sH, dX, dY, sW, sH); - - // --- NEW: BLUE MARBLE EFFECT --- - if (state.blueMarbleEnabled) { - const imageData = chunkCtx.getImageData(0, 0, this.tileSize, this.tileSize); - const data = imageData.data; - for (let pixelY = 0; pixelY < this.tileSize; pixelY++) { - for (let pixelX = 0; pixelX < this.tileSize; pixelX++) { - const canvasX = pixelX; - const canvasY = pixelY; - const imageX = canvasX - dX; - const imageY = canvasY - dY; - - if ((imageX + imageY) % 2 === 0) { - const index = (canvasY * this.tileSize + canvasX) * 4; - if (data[index + 3] > 0) { - data[index + 3] = 0; - } - } - } - } - chunkCtx.putImageData(imageData, 0, 0); - } - - const chunkBitmap = await chunkCanvas.transferToImageBitmap(); - this.chunkedTiles.set(tileKey, chunkBitmap); + tilesToProcess.push({ tx, ty }); } } - console.log(`Overlay processed into ${this.chunkedTiles.size} chunks.`); + // Process tiles in batches with yielding + for (let i = 0; i < tilesToProcess.length; i += batchSize) { + const batch = tilesToProcess.slice(i, i + batchSize); + + await Promise.all(batch.map(async ({ tx, ty }) => { + const tileKey = `${tx},${ty}`; + const chunkBitmap = await this._processTile(tx, ty, imageWidth, imageHeight, startPixelX, startPixelY, startRegionX, startRegionY); + if (chunkBitmap) { + this.chunkedTiles.set(tileKey, chunkBitmap); + } + })); + + // Yield control to prevent blocking + if (i + batchSize < tilesToProcess.length) { + await new Promise(resolve => setTimeout(resolve, 0)); + } + } + + const processingTime = performance.now() - startTime; + console.log(`✅ Overlay processed ${this.chunkedTiles.size} tiles in ${Math.round(processingTime)}ms`); } - // --- OVERLAY UPDATE: Simplified compositing logic for solid, semi-transparent overlay --- + async _processTile(tx, ty, imageWidth, imageHeight, startPixelX, startPixelY, startRegionX, startRegionY) { + const tileKey = `${tx},${ty}`; + + // Calculate the portion of the image that overlaps with this tile + const imgStartX = (tx - startRegionX) * this.tileSize - startPixelX; + const imgStartY = (ty - startRegionY) * this.tileSize - startPixelY; + + // Crop coordinates within the source image + const sX = Math.max(0, imgStartX); + const sY = Math.max(0, imgStartY); + const sW = Math.min(imageWidth - sX, this.tileSize - (sX - imgStartX)); + const sH = Math.min(imageHeight - sY, this.tileSize - (sY - imgStartY)); + + if (sW <= 0 || sH <= 0) return null; + + // Destination coordinates on the new chunk canvas + const dX = Math.max(0, -imgStartX); + const dY = Math.max(0, -imgStartY); + + const chunkCanvas = new OffscreenCanvas(this.tileSize, this.tileSize); + const chunkCtx = chunkCanvas.getContext('2d'); + chunkCtx.imageSmoothingEnabled = false; + + chunkCtx.drawImage(this.imageBitmap, sX, sY, sW, sH, dX, dY, sW, sH); + + // --- OPTIMIZED: Blue marble effect with faster pixel manipulation --- + if (state.blueMarbleEnabled) { + const imageData = chunkCtx.getImageData(dX, dY, sW, sH); + const data = imageData.data; + + // Faster pixel manipulation using typed arrays + for (let i = 0; i < data.length; i += 4) { + const pixelIndex = i / 4; + const pixelY = Math.floor(pixelIndex / sW); + const pixelX = pixelIndex % sW; + + if ((pixelX + pixelY) % 2 === 0 && data[i + 3] > 0) { + data[i + 3] = 0; // Set alpha to 0 + } + } + + chunkCtx.putImageData(imageData, dX, dY); + } + + return await chunkCanvas.transferToImageBitmap(); + } + + // --- OVERLAY UPDATE: Optimized compositing with caching --- async processAndRespondToTileRequest(eventData) { const { endpoint, blobID, blobData } = eventData; @@ -1124,21 +1194,12 @@ const chunkBitmap = this.chunkedTiles.get(tileKey); if (chunkBitmap) { try { - const originalTileBitmap = await createImageBitmap(blobData); - const canvas = new OffscreenCanvas(originalTileBitmap.width, originalTileBitmap.height); - const ctx = canvas.getContext('2d'); - ctx.imageSmoothingEnabled = false; - - // Draw original tile first - ctx.drawImage(originalTileBitmap, 0, 0); - - // Set opacity and draw our solid overlay chunk on top - ctx.globalAlpha = state.overlayOpacity; - ctx.drawImage(chunkBitmap, 0, 0); - - finalBlob = await canvas.convertToBlob({ type: 'image/png' }); + // Use faster compositing for better performance + finalBlob = await this._compositeTileOptimized(blobData, chunkBitmap); } catch (e) { console.error("Error compositing overlay:", e); + // Fallback to original tile on error + finalBlob = blobData; } } } @@ -1151,6 +1212,29 @@ blobData: finalBlob }, '*'); } + + async _compositeTileOptimized(originalBlob, overlayBitmap) { + const originalBitmap = await createImageBitmap(originalBlob); + const canvas = new OffscreenCanvas(originalBitmap.width, originalBitmap.height); + const ctx = canvas.getContext('2d'); + + // Disable antialiasing for pixel-perfect rendering + ctx.imageSmoothingEnabled = false; + + // Draw original tile first + ctx.drawImage(originalBitmap, 0, 0); + + // Set opacity and draw overlay with optimized blend mode + ctx.globalAlpha = state.overlayOpacity; + ctx.globalCompositeOperation = 'source-over'; + ctx.drawImage(overlayBitmap, 0, 0); + + // Use faster blob conversion with compression settings + return await canvas.convertToBlob({ + type: 'image/png', + quality: 0.95 // Slight compression for faster processing + }); + } } const overlayManager = new OverlayManager(); @@ -1163,6 +1247,7 @@ let tokenPromise = new Promise((resolve) => { _resolveToken = resolve }) let retryCount = 0 const MAX_RETRIES = 10 + const MAX_BATCH_RETRIES = 10 // Maximum attempts for batch sending const TOKEN_LIFETIME = 240000 // 4 minutes (tokens typically last 5 min, use 4 for safety) function setTurnstileToken(t) { @@ -5566,7 +5651,13 @@ for (let x = y === startRow ? startCol : 0; x < width; x++) { if (state.stopFlag) { if (pixelBatch && pixelBatch.pixels.length > 0) { - await sendPixelBatch(pixelBatch.pixels, pixelBatch.regionX, pixelBatch.regionY); + console.log(`🎯 Sending final batch before stop with ${pixelBatch.pixels.length} pixels`); + const success = await sendBatchWithRetry(pixelBatch.pixels, pixelBatch.regionX, pixelBatch.regionY); + if (success) { + pixelBatch.pixels.forEach(() => { state.paintedPixels++; }); + state.currentCharges -= pixelBatch.pixels.length; + updateStats(); + } } state.lastPosition = { x, y } updateUI("paintingPaused", "warning", { x, y }) @@ -5612,23 +5703,8 @@ pixelBatch.regionY !== regionY + adderY) { if (pixelBatch && pixelBatch.pixels.length > 0) { - let success = await sendPixelBatch(pixelBatch.pixels, pixelBatch.regionX, pixelBatch.regionY); - if (success === "token_error") { - updateUI("captchaSolving", "warning"); - try { - await handleCaptcha(); - success = await sendPixelBatch(pixelBatch.pixels, pixelBatch.regionX, pixelBatch.regionY); - if (success === "token_error") { - updateUI("captchaFailed", "error"); - state.stopFlag = true; - break outerLoop; - } - } catch (e) { - updateUI("captchaFailed", "error"); - state.stopFlag = true; - break outerLoop; - } - } + const success = await sendBatchWithRetry(pixelBatch.pixels, pixelBatch.regionX, pixelBatch.regionY); + if (success) { pixelBatch.pixels.forEach((p) => { state.paintedPixels++; }); state.currentCharges -= pixelBatch.pixels.length; @@ -5648,12 +5724,11 @@ } updateStats(); } else { - // If batch failed, don't mark pixels as painted so they can be retried - console.warn(`⚠️ Batch failed for region ${pixelBatch.regionX},${pixelBatch.regionY} with ${pixelBatch.pixels.length} pixels. Will retry later.`); - // Wait a bit before continuing to avoid rapid retries - await Utils.sleep(1000); + // If batch failed after all retries, stop painting to prevent infinite loops + console.error(`❌ Batch failed permanently after retries. Stopping painting.`); + state.stopFlag = true; + break outerLoop; } - } pixelBatch = { @@ -5672,23 +5747,7 @@ }); if (pixelBatch.pixels.length >= Math.floor(state.currentCharges)) { - let success = await sendPixelBatch(pixelBatch.pixels, pixelBatch.regionX, pixelBatch.regionY); - if (success === "token_error") { - updateUI("captchaSolving", "warning"); - try { - await handleCaptcha(); - success = await sendPixelBatch(pixelBatch.pixels, pixelBatch.regionX, pixelBatch.regionY); - if (success === "token_error") { - updateUI("captchaFailed", "error"); - state.stopFlag = true; - break outerLoop; - } - } catch (e) { - updateUI("captchaFailed", "error"); - state.stopFlag = true; - break outerLoop; - } - } + const success = await sendBatchWithRetry(pixelBatch.pixels, pixelBatch.regionX, pixelBatch.regionY); if (success) { pixelBatch.pixels.forEach((pixel) => { @@ -5712,10 +5771,10 @@ await Utils.sleep(totalDelay) } } else { - // If batch failed, don't mark pixels as painted so they can be retried - console.warn(`⚠️ Batch failed with ${pixelBatch.pixels.length} pixels. Will retry later.`); - // Wait a bit before continuing to avoid rapid retries - await Utils.sleep(1000); + // If batch failed after all retries, stop painting to prevent infinite loops + console.error(`❌ Batch failed permanently after retries. Stopping painting.`); + state.stopFlag = true; + break outerLoop; } pixelBatch.pixels = []; @@ -5745,7 +5804,7 @@ } if (pixelBatch && pixelBatch.pixels.length > 0 && !state.stopFlag) { - const success = await sendPixelBatch(pixelBatch.pixels, pixelBatch.regionX, pixelBatch.regionY); + const success = await sendBatchWithRetry(pixelBatch.pixels, pixelBatch.regionX, pixelBatch.regionY); if (success) { pixelBatch.pixels.forEach((pixel) => { state.paintedPixels++ @@ -5757,8 +5816,8 @@ await Utils.sleep(totalDelay) } } else { - // If final batch failed, log it but don't retry to avoid infinite loops - console.warn(`⚠️ Final batch failed with ${pixelBatch.pixels.length} pixels. These pixels will need to be painted on next run.`); + // If final batch failed after retries, log it + console.warn(`⚠️ Final batch failed with ${pixelBatch.pixels.length} pixels after all retries.`); } } } finally { @@ -5793,6 +5852,50 @@ updateStats() } + // Helper function to retry batch until success with exponential backoff + async function sendBatchWithRetry(pixels, regionX, regionY, maxRetries = MAX_BATCH_RETRIES) { + let attempt = 0; + while (attempt < maxRetries && !state.stopFlag) { + attempt++; + console.log(`🔄 Attempting to send batch (attempt ${attempt}/${maxRetries}) for region ${regionX},${regionY} with ${pixels.length} pixels`); + + const result = await sendPixelBatch(pixels, regionX, regionY); + + if (result === true) { + console.log(`✅ Batch succeeded on attempt ${attempt}`); + return true; + } else if (result === "token_error") { + console.log(`🔑 Token error on attempt ${attempt}, regenerating...`); + updateUI("captchaSolving", "warning"); + try { + await handleCaptcha(); + // Don't count token regeneration as a failed attempt + attempt--; + continue; + } catch (e) { + console.error(`❌ Token regeneration failed on attempt ${attempt}:`, e); + updateUI("captchaFailed", "error"); + // Wait longer before retrying after token failure + await Utils.sleep(5000); + } + } else { + console.warn(`⚠️ Batch failed on attempt ${attempt}, retrying...`); + // Exponential backoff with jitter + const baseDelay = Math.min(1000 * Math.pow(2, attempt - 1), 30000); // Max 30s + const jitter = Math.random() * 1000; // Add up to 1s random delay + await Utils.sleep(baseDelay + jitter); + } + } + + if (attempt >= maxRetries) { + console.error(`❌ Batch failed after ${maxRetries} attempts (MAX_BATCH_RETRIES=${MAX_BATCH_RETRIES}). This will stop painting to prevent infinite loops.`); + updateUI("paintingError", "error"); + return false; + } + + return false; + } + async function sendPixelBatch(pixelBatch, regionX, regionY) { let token = turnstileToken;