Files
brittle/src-tauri/dist/render-system.js
2026-04-01 01:27:51 +02:00

339 lines
13 KiB
JavaScript

/**
* render-system.ts — Advanced rendering system with performance optimization
*
* Manages the complete rendering pipeline with:
* - Double buffering for flicker-free rendering
* - Adaptive quality based on performance
* - Intelligent page caching
* - Smooth zoom and scroll handling
*/
import { PerformanceManager } from './performance-manager.js';
class RenderSystem {
constructor(pdfDoc, container, pageManager) {
this.pdfDoc = pdfDoc;
this.container = container;
this.pageManager = pageManager;
try {
this.performanceManager = new PerformanceManager();
}
catch (error) {
console.error("Failed to create PerformanceManager:", error);
// Fallback: create a minimal performance manager
this.performanceManager = {
getPerformanceStats: () => ({ fps: 60, renderQuality: 1.0, adaptiveMode: false }),
isPerformanceCritical: () => false,
cleanup: () => { }
};
}
// Render state
this.currentScale = 1.0;
this.targetScale = 1.0;
this.renderQuality = 1.0;
this.visiblePages = new Set();
this.bufferPages = new Set();
this.renderQueue = [];
this.activeRenders = new Map();
this.pageCache = new Map(); // { "page_scale": {bitmap, timestamp, quality} }
this.maxCacheSize = 20; // Max pages in cache
this.cacheSize = 0;
// Performance metrics
this.lastRenderTime = 0;
this.renderCount = 0;
// Setup event listeners
this.setupEventListeners();
}
setupEventListeners() {
// Performance-based quality adjustments
setInterval(() => {
this.adjustQualityBasedOnPerformance();
}, 1000);
}
setVisibility(visiblePages, bufferPages) {
this.visiblePages = new Set(visiblePages);
this.bufferPages = new Set(bufferPages);
this.scheduleRenders();
}
setScale(scale) {
this.targetScale = scale;
// Smooth transition for zoom
if (Math.abs(this.targetScale - this.currentScale) > 0.01) {
this.startZoomTransition();
}
else {
this.currentScale = this.targetScale;
this.scheduleRenders();
}
}
startZoomTransition() {
const startScale = this.currentScale;
const endScale = this.targetScale;
const startTime = performance.now();
const duration = 150; // 150ms transition
const animate = (currentTime) => {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
// Ease-in-out animation
const easedProgress = this.easeInOutCubic(progress);
this.currentScale = startScale + (endScale - startScale) * easedProgress;
// Use lower quality during transition
const transitionQuality = this.renderQuality * 0.8;
this.scheduleRenders(transitionQuality);
if (progress < 1) {
requestAnimationFrame(animate);
}
else {
this.currentScale = endScale;
// Final high-quality render after transition
this.scheduleRenders(this.renderQuality);
}
};
requestAnimationFrame(animate);
}
easeInOutCubic(t) {
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}
scheduleRenders(overrideQuality = null) {
const quality = overrideQuality !== null ? overrideQuality : this.renderQuality;
const scale = this.currentScale;
// Cancel renders for pages that are no longer needed
this.cancelStaleRenders();
// Prioritize visible pages, then buffer pages
const allPages = [...this.visiblePages, ...this.bufferPages];
allPages.forEach(pageNum => {
const cacheKey = this.getCacheKey(pageNum, scale);
// Check if we need to render this page
if (this.shouldRenderPage(pageNum, scale, quality)) {
this.queueRender(pageNum, scale, quality);
}
});
// Process the render queue
this.processRenderQueue();
}
shouldRenderPage(pageNum, scale, quality) {
const cacheKey = this.getCacheKey(pageNum, scale);
// Check if already rendering
if (this.activeRenders.has(pageNum)) {
return false;
}
// Check cache with acceptable quality
const cached = this.pageCache.get(cacheKey);
if (cached) {
// If cached quality is acceptable, no need to re-render
if (cached.quality >= quality * 0.9) {
this.useCachedPage(pageNum, scale, cached.bitmap);
return false;
}
}
return true;
}
queueRender(pageNum, scale, quality) {
// Avoid duplicate queue entries
if (this.renderQueue.some(item => item.pageNum === pageNum && Math.abs(item.scale - scale) < 0.01)) {
return;
}
// Prioritize visible pages
const priority = this.visiblePages.has(pageNum) ? 1 : 2;
this.renderQueue.push({ pageNum, scale, quality, priority });
// Sort by priority (visible pages first)
this.renderQueue.sort((a, b) => a.priority - b.priority);
}
processRenderQueue() {
if (this.renderQueue.length === 0)
return;
// Limit concurrent renders based on performance
const maxConcurrent = this.performanceManager.isPerformanceCritical() ? 2 : 4;
while (this.activeRenders.size < maxConcurrent && this.renderQueue.length > 0) {
const renderTask = this.renderQueue.shift();
if (renderTask) {
this.startRender(renderTask.pageNum, renderTask.scale, renderTask.quality);
}
}
}
async startRender(pageNum, scale, quality) {
this.activeRenders.set(pageNum, true);
let page = null;
try {
page = await this.pdfDoc.getPage(pageNum);
// Apply quality-based scaling
const effectiveScale = scale * quality;
const viewport = page.getViewport({ scale: effectiveScale });
// Validate canvas dimensions
const width = Math.round(viewport.width);
const height = Math.round(viewport.height);
if (isNaN(width) || isNaN(height) || width <= 0 || height <= 0) {
throw new Error(`Invalid canvas dimensions: ${width}x${height}`);
}
// Create offscreen canvas
const offscreen = new OffscreenCanvas(width, height);
const context = offscreen.getContext('2d');
if (!context) {
throw new Error('Failed to get 2D context for offscreen canvas');
}
// Render with quality settings
await page.render({
canvasContext: context,
viewport: viewport,
// Use lower quality rendering when performance is critical
renderQuality: this.performanceManager.isPerformanceCritical() ? 'low' : 'high'
}).promise;
// Transfer bitmap
const bitmap = offscreen.transferToImageBitmap();
// Cache the result
const cacheKey = this.getCacheKey(pageNum, scale);
this.cachePage(cacheKey, bitmap, quality);
// Update display
this.updatePageDisplay(pageNum, bitmap);
}
catch (error) {
if (error && typeof error === 'object' && 'name' in error && error.name !== "RenderingCancelledException") {
console.warn(`[RenderSystem] Render failed for page ${pageNum}:`, error);
}
}
finally {
this.activeRenders.delete(pageNum);
page?.cleanup();
// Process next in queue
this.processRenderQueue();
}
}
cachePage(cacheKey, bitmap, quality) {
// Evict if cache is full
if (this.cacheSize >= this.maxCacheSize) {
this.evictOldestCacheEntry();
}
this.pageCache.set(cacheKey, {
bitmap,
quality,
timestamp: Date.now(),
accessTime: Date.now()
});
this.cacheSize++;
// Update access time for LRU
this.updateCacheAccess(cacheKey);
}
evictOldestCacheEntry() {
if (this.pageCache.size === 0)
return;
// Find least recently used
let oldestKey = null;
let oldestTime = Infinity;
this.pageCache.forEach((entry, key) => {
if (entry.accessTime < oldestTime) {
oldestTime = entry.accessTime;
oldestKey = key;
}
});
if (oldestKey) {
const entry = this.pageCache.get(oldestKey);
if (entry) {
entry.bitmap.close(); // Free memory
}
this.pageCache.delete(oldestKey);
this.cacheSize--;
}
}
updateCacheAccess(cacheKey) {
const entry = this.pageCache.get(cacheKey);
if (entry) {
entry.accessTime = Date.now();
}
}
useCachedPage(pageNum, scale, bitmap) {
// Create a copy of the bitmap for display
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
const context = canvas.getContext('2d');
if (context) {
context.drawImage(bitmap, 0, 0);
const displayBitmap = canvas.transferToImageBitmap();
this.updatePageDisplay(pageNum, displayBitmap);
}
}
updatePageDisplay(pageNum, bitmap) {
// Use the EnhancedPageManager's onRendered method to handle display
// This ensures proper integration with the page manager's state and canvas management
const gen = this.pageManager.renderGen;
const quality = this.renderQuality; // Use current render quality
this.pageManager.onRendered(pageNum, gen, bitmap, quality);
}
cancelStaleRenders() {
// Cancel renders for pages that are no longer visible or buffered
const neededPages = new Set([...this.visiblePages, ...this.bufferPages]);
this.activeRenders.forEach((_, pageNum) => {
if (!neededPages.has(pageNum)) {
// This render is no longer needed
this.activeRenders.delete(pageNum);
}
});
// Also clean up render queue
this.renderQueue = this.renderQueue.filter(task => neededPages.has(task.pageNum));
}
adjustQualityBasedOnPerformance() {
const stats = this.performanceManager.getPerformanceStats();
// If performance is critical, we may have already adjusted quality
// Here we can make additional adjustments based on render load
if (this.activeRenders.size > 4 && stats.fps < 50) {
// Too many active renders and low FPS - reduce quality more
const newQuality = Math.max(0.5, this.renderQuality - 0.1);
if (newQuality < this.renderQuality) {
this.setRenderQuality(newQuality);
}
}
}
setRenderQuality(quality) {
this.renderQuality = Math.max(0.3, Math.min(1.0, quality));
// Re-render visible pages with new quality
if (this.visiblePages.size > 0) {
this.scheduleRenders(this.renderQuality);
}
}
cleanupMemory() {
// Aggressive cleanup - keep only visible pages
const visibleCacheKeys = new Set();
this.visiblePages.forEach(pageNum => {
const cacheKey = this.getCacheKey(pageNum, this.currentScale);
visibleCacheKeys.add(cacheKey);
});
// Remove all non-visible pages from cache
this.pageCache.forEach((entry, cacheKey) => {
if (!visibleCacheKeys.has(cacheKey)) {
entry.bitmap.close();
this.pageCache.delete(cacheKey);
this.cacheSize--;
}
});
// Force garbage collection if available
if (window.gc) {
window.gc();
}
}
getCacheKey(pageNum, scale) {
// Round scale to 2 decimal places for caching
const roundedScale = Math.round(scale * 100) / 100;
return `${pageNum}_${roundedScale}`;
}
async handleRenderRequest(pageNum, scale, vpWidth, vpHeight, quality) {
// Handle render request from PageManager
// This integrates with the PageManager's rendering pipeline
await this.startRender(pageNum, scale, quality);
}
cleanup() {
// Clean up all resources
this.pageCache.forEach(entry => {
entry.bitmap.close();
});
this.activeRenders.clear();
this.renderQueue = [];
this.performanceManager.cleanup();
}
getPerformanceStats() {
return {
...this.performanceManager.getPerformanceStats(),
activeRenders: this.activeRenders.size,
cacheSize: this.cacheSize,
renderQueueLength: this.renderQueue.length
};
}
}
// Export for ES6 modules
export { RenderSystem };
//# sourceMappingURL=render-system.js.map