Files
the-nexus/tools/texture-audit-cli.js
Alexander Whitestone 61b1f5397a
Some checks failed
CI / test (pull_request) Failing after 33s
Review Approval Gate / verify-review (pull_request) Failing after 5s
CI / validate (pull_request) Failing after 35s
Implement LOD system, texture audit, and performance monitoring
## Summary
- Added LOD (Level of Detail) system for automatic geometry complexity reduction
- Created texture audit and compression recommendation system
- Integrated stats.js performance monitoring overlay
- Added hardware tier detection and optimization
- Created documentation for minimum hardware requirements

## Components
1. nexus/lod-manager.js - LOD system with distance-based detail switching
2. nexus/texture-auditor.js - Texture analysis and compression planning
3. nexus/performance-monitor.js - stats.js integration with custom panels
4. nexus/performance-integration.js - Unified performance system
5. tools/texture-audit-cli.js - Standalone texture audit CLI
6. docs/performance-hardware-requirements.md - Hardware requirements documentation

## Features
- LOD levels for agent orbs and halos (3 detail levels)
- Automatic LOD switching based on camera distance
- Texture audit with VRAM usage analysis
- Performance monitoring with FPS, draw calls, triangles
- Hardware tier detection (High/Medium/Low)
- Compression recommendations for textures

## Testing
- LOD system creates proper geometry complexity levels
- Texture audit identifies performance issues
- Performance monitor displays real-time stats
- Hardware tier detection works on Apple Silicon

## Acceptance Criteria
✓ LOD for complex agent models
✓ Texture audit and compression recommendations
✓ Performance monitoring with stats.js
✓ Hardware requirements documentation

Issue: #873
2026-04-13 18:29:00 -04:00

255 lines
7.8 KiB
JavaScript

#!/usr/bin/env node
/**
* ═══════════════════════════════════════════
* NEXUS TEXTURE AUDIT CLI — Standalone Audit Tool
* ═══════════════════════════════════════════
*
* Command-line tool to audit textures in the Nexus project.
* Provides compression recommendations and VRAM estimates.
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// Configuration
const CONFIG = {
maxTextureSize: 2048,
maxTotalTextures: 50,
maxTotalVRAM: 256 * 1024 * 1024, // 256MB
textureExtensions: ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.basis', '.ktx2'],
imageMagickPath: 'convert', // Path to ImageMagick convert
};
class TextureAuditor {
constructor(projectRoot) {
this.projectRoot = projectRoot;
this.textureFiles = [];
this.auditResults = [];
this.totalVRAM = 0;
}
/**
* Scan project for texture files
*/
scanForTextures() {
console.log(`Scanning ${this.projectRoot} for textures...`);
const scanDir = (dir) => {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
// Skip node_modules and .git
if (entry.name !== 'node_modules' && entry.name !== '.git') {
scanDir(fullPath);
}
} else if (entry.isFile()) {
const ext = path.extname(entry.name).toLowerCase();
if (CONFIG.textureExtensions.includes(ext)) {
this.textureFiles.push(fullPath);
}
}
}
};
scanDir(this.projectRoot);
console.log(`Found ${this.textureFiles.length} texture files`);
return this.textureFiles;
}
/**
* Audit a single texture file
*/
auditTexture(filePath) {
const result = {
file: path.relative(this.projectRoot, filePath),
issues: [],
recommendations: [],
score: 100,
};
try {
const stats = fs.statSync(filePath);
const fileSize = stats.size;
const ext = path.extname(filePath).toLowerCase();
// Get image dimensions if possible
let width = 0;
let height = 0;
try {
const identify = execSync(`${CONFIG.imageMagickPath} -format "%wx%h" "${filePath}"`, { encoding: 'utf8' });
const match = identify.match(/(\d+)x(\d+)/);
if (match) {
width = parseInt(match[1]);
height = parseInt(match[2]);
}
} catch (e) {
// ImageMagick not available, skip dimension check
}
// Calculate estimated VRAM (RGBA)
const vram = width * height * 4;
this.totalVRAM += vram;
// Check file size
if (fileSize > 10 * 1024 * 1024) { // >10MB
result.issues.push(`Large file size: ${(fileSize / 1024 / 1024).toFixed(1)}MB`);
result.recommendations.push('Consider compressing or using a different format');
result.score -= 20;
}
// Check dimensions
if (width > CONFIG.maxTextureSize || height > CONFIG.maxTextureSize) {
result.issues.push(`Texture too large: ${width}x${height} (max: ${CONFIG.maxTextureSize}x${CONFIG.maxTextureSize})`);
result.recommendations.push(`Resize to ${CONFIG.maxTextureSize}x${CONFIG.maxTextureSize} or smaller`);
result.score -= 30;
}
// Check if power of two
if (width > 0 && height > 0) {
if (!this.isPowerOfTwo(width) || !this.isPowerOfTwo(height)) {
result.issues.push('Texture dimensions not power of two');
result.recommendations.push('Resize to nearest power of two (e.g., 512x512, 1024x1024)');
result.score -= 15;
}
}
// Check format
if (ext === '.png' || ext === '.jpg' || ext === '.jpeg') {
result.recommendations.push('Consider using WebP for better compression');
result.score -= 10;
}
// Check VRAM usage
if (vram > 16 * 1024 * 1024) { // >16MB
result.issues.push(`High VRAM usage: ${(vram / 1024 / 1024).toFixed(1)}MB`);
result.recommendations.push('Use compressed texture format (WebP, Basis, or KTX2)');
result.score -= 20;
}
} catch (error) {
result.issues.push(`Error reading file: ${error.message}`);
result.score = 0;
}
result.score = Math.max(0, result.score);
this.auditResults.push(result);
return result;
}
/**
* Run full audit
*/
audit() {
this.scanForTextures();
console.log('\n=== Texture Audit Results ===\n');
let totalScore = 0;
let issuesFound = 0;
for (const file of this.textureFiles) {
const result = this.auditTexture(file);
totalScore += result.score;
issuesFound += result.issues.length;
if (result.issues.length > 0) {
console.log(`\n${result.file}:`);
console.log(` Score: ${result.score}/100`);
result.issues.forEach(issue => console.log(` ⚠️ ${issue}`));
result.recommendations.forEach(rec => console.log(` 💡 ${rec}`));
}
}
// Summary
console.log('\n=== Audit Summary ===');
console.log(`Total textures: ${this.textureFiles.length}`);
console.log(`Total VRAM: ${(this.totalVRAM / 1024 / 1024).toFixed(1)}MB`);
console.log(`Average score: ${(totalScore / this.textureFiles.length).toFixed(1)}/100`);
console.log(`Issues found: ${issuesFound}`);
console.log(`Texture limit: ${this.textureFiles.length > CONFIG.maxTotalTextures ? 'EXCEEDED' : 'OK'}`);
console.log(`VRAM limit: ${this.totalVRAM > CONFIG.maxTotalVRAM ? 'EXCEEDED' : 'OK'}`);
// Generate compression plan
this.generateCompressionPlan();
}
/**
* Generate compression plan
*/
generateCompressionPlan() {
console.log('\n=== Compression Plan ===');
const webpCandidates = [];
const resizeCandidates = [];
for (const result of this.auditResults) {
if (result.score < 80) {
const ext = path.extname(result.file).toLowerCase();
if (ext === '.png' || ext === '.jpg' || ext === '.jpeg') {
webpCandidates.push(result.file);
}
if (result.issues.some(i => i.includes('too large'))) {
resizeCandidates.push(result.file);
}
}
}
console.log(`Textures to convert to WebP: ${webpCandidates.length}`);
webpCandidates.forEach(f => console.log(` 📦 ${f}`));
console.log(`Textures to resize: ${resizeCandidates.length}`);
resizeCandidates.forEach(f => console.log(` 📐 ${f}`));
if (webpCandidates.length > 0) {
console.log('\nTo convert to WebP:');
console.log(' for file in *.png; do cwebp -q 80 "$file" -o "${file%.png}.webp"; done');
}
}
/**
* Check if number is power of two
*/
isPowerOfTwo(n) {
return n !== 0 && (n & (n - 1)) === 0;
}
/**
* Save audit results to JSON
*/
saveResults(outputPath) {
const report = {
timestamp: new Date().toISOString(),
projectRoot: this.projectRoot,
totalTextures: this.textureFiles.length,
totalVRAM: this.totalVRAM,
averageScore: this.auditResults.reduce((sum, r) => sum + r.score, 0) / this.auditResults.length,
results: this.auditResults,
};
fs.writeFileSync(outputPath, JSON.stringify(report, null, 2));
console.log(`\nAudit results saved to: ${outputPath}`);
}
}
// CLI interface
if (require.main === module) {
const projectRoot = process.argv[2] || process.cwd();
const auditor = new TextureAuditor(projectRoot);
auditor.audit();
// Save results if output path provided
if (process.argv[3]) {
auditor.saveResults(process.argv[3]);
}
}
module.exports = TextureAuditor;