Joxblox v1.5.0
Added
- Scan: right-click any result row for a context menu with
Copy(Asset ID / Asset Reference / SHA256) andTag(Downscale, Duplicated, Decimate, Atlas, Remove Alpha, Remove). Active tags show in a newTagscolumn. AHTML Tag Reportbutton on the scan toolbar then generates a self-contained HTML page with one section per tag, each showing the tagged assets as image cards (asset ID, type, dimensions, GPU texture memory, instance path). Tags live in memory for the session and are not persisted across rescans - Scan:
Duplicatedtag opens a picker dialog instead of toggling on a single row. The right-clicked asset is preselected at the top with its preview already showing; the dialog has a side-by-side layout (candidate table on the left, asset preview on the right). Single-click any row to swap the preview to that asset; double-click to add or remove it from the group. Candidates are sorted by perceptual-hash similarity (dHash Hamming distance) to the primary asset, with aSimilaritycolumn showing each row's100% (0)-style score so the user can scan the top of the list for near-identical content; rows that can't be hashed (no decodable image bytes, or the primary itself is a non-image) fall back to SHA256 clustering. Editing an existing group reopens the same dialog with all current members preselected. The HTML Tag Report renders each curated group as its ownGroup N — N copiesblock so reviewers can see at a glance which copies are flagged as variants of which - Scan: new
PDF Tag Reportbutton next toHTML Tag Report— exports the same per-tag breakdown (and curatedGroup N — N copiessub-blocks for Duplicated) as a self-contained PDF with embedded thumbnails. Built withgofpdfso it ships in the binary; no Chrome / external renderer required. The asset card grid wraps at four cards per A4 row, paginates automatically, and falls back to a "(no preview)" placeholder when an asset'sResourceisn't a PDF-embeddable format - Scan: SHA256-detected duplicate clusters auto-tag as
Duplicatedgroups when a scan completes — the same grouping the picker dialog produces, but seeded from byte-identical files the loader already deduplicated. Manual groups created via the dialog always win: an auto-tag run skips any cluster whose members overlap a user-curated group, and re-running auto-tag on an already-grouped cluster is a no-op - Scan: new
Show untaggedfilter checkbox next toShow duplicatesandShow large textures— hides any row that already carries at least one tag, so triagers can drill into "rows that still need a decision" after burning through the obvious tags. The existingShow only duplicatescheckbox is renamed toShow duplicatesfor symmetry - Scan:
Path Filtercheckbox now defaults on, scoped toWorkspace.*andMaterialService.*, so fresh scans skip the editor-only / tooling tail of an rbxl by default. Untick it to walk the full instance tree - Scan: default sort is now
GPU Memory(descending) instead ofSelf Size— the metric that drives in-game VRAM cost surfaces first, so triagers see the heaviest textures at the top without having to re-sort. Heatmap variant still defaults toTotal Byte Size
Fixed
- Scan: when an asset's full preview was lazy-loaded after row selection (because the streaming scan only got a thumbnail or no Source), the freshly-loaded
Resource/DownloadBytes/FileSHA256/ dimensions weren't being persisted back into the explorer's master row list. That made the duplicate-picker dialog'sSimilaritycolumn render-for every row even when the primary and candidates had matching SHA256s — the SHA fast-path saw an empty primary SHA and the dHash path saw an empty primaryResource. The lazy-load completion now writes the loaded result back intoallResults(matched byAssetReferenceKey) so subsequent reads see the populated row - Scan: duplicate-picker dialog now synchronously lazy-fetches the primary's full preview when its bytes / SHA aren't already in
allResults, so theSimilaritycolumn populates even on the first open before any async load has happened. The status line gained aRanked X/Y candidatescounter so users can tell at a glance whether the column is empty due to no matches versus a missing primary preview - Scan: similarity dHash now falls back to the rendered
Resource.Content()(PNG/JPEG) when the rawDownloadBytesfrom AssetDelivery aren't decodable by Go's stdlibimagepackage — Roblox sometimes ships textures in formats stdlib doesn't read (KTX2, custom containers), and the rendered preview is the reliable fallback. Previously every row whose primary fell into that bucket rendered "-" in the Similarity column even when matches existed - Scan: similarity ranking for normal-map slots now hashes the R and G channels independently and sums their Hamming distances, instead of using the standard luminance dHash. Normal maps encode XY surface deflection in R/G with the Z (blue) channel near-uniform, which dominated luminance and made every normal map look like every other one to the old hash; the dual-channel variant catches X-only vs Y-only deflection patterns that luminance collapses. Detected automatically from the row's
PropertyName(NormalMap / NormalMapContent) and applied to every candidate so distances stay comparable; theSimilaritycolumn scales its percentage to the doubled bit space (112 vs 56) so identical files still read100% (0) - Report Generation:
Export JSONbutton — saves the full graded report (overall, per-grade letters/scores/values, summary stats, per-cell avg/p90/max metrics, mismatched-PBR / oversized-texture / duplicate-group drill-downs, asset type, source file metadata) as a single JSON file via the native save dialog. Versioned schema (format_version) so a future viewer / share-link server can render historical exports without depending on internal types
Added
- Report Generation: per-asset-type hard cap on individual texture GPU-memory footprint.
DE Mapreports flag any texture whose largest per-slot GPU memory (BC1 or BC3, with mips) exceeds the configured limit and force the overall grade to F regardless of every other metric. A red banner above the grade table shows the offender count with aViewbutton listing each texture's asset ID, resolution, GPU-bytes/format, and instance path, plus anIgnorebutton that suppresses the F override and reveals the natural grade (with aRestorebutton to undo) - Scan: top-bar
Asset typeselector. When set to a type with a configured GPU-memory ban (e.g.DE Map), result rows whose largest per-slot GPU footprint exceeds the limit are highlighted in red so banned textures are immediately obvious in scan-mode triage - Scan: new
Materialssub-tab next to the asset table. Shows engine-deduplicated PBR materials — one row per unique (color/normal/metalness/roughness) asset combo — with the authored slot sizes, the engine-effective normal size (upscaled to its largest paired color), the MR-pack size (max of group normal/color/M/R), the BC1+BC3 GPU bytes the engine actually allocates for that material's share, and how many SurfaceAppearance instances reuse the bundle. A header summary shows the engine GPU-memory total — dedupes shared normal assets, accounts for blank-MR packs, so it lines up with the report tab'sGPU Texture Memoryheadline rather than the asset-level sum - Material variant warnings:
Ignorebutton next to the existingViewbutton so the warning banner can be dismissed for the current load without dismissing the underlying data
Changed
- Report Generation:
View in Scanno longer re-fetches every asset — the previews already loaded for the report are handed off directly to the Scan tab, so the table appears immediately. The ScanAsset typeselector is auto-set to match the report - Report Generation: spatial grades now show two columns —
typical(content-weighted average, Σv²/Σv — the cell value an average unit of content sits in, ignoring the long tail of near-empty cells) andmax/cell. Theavg/cellandp90/cellcolumns are gone - Report Generation: row grades are now the average of the typical-cell sub-score and the p90-cell sub-score (max no longer contributes to the grade — it's informational only)
- Report Generation:
Mismatched PBR Mapsnow counts unique asset combos rather than raw SurfaceAppearance instances — multiple instances sharing the same (color/normal/metalness/roughness) bundle collapse to one entry, since fixing the asset fixes every usage. The grade threshold grades against the unique-combo count, the value cell shows e.g.3 (8.00 MB), and theViewdialog shows one row per combo with an×N instances / save N MBsuffix, sorted by per-combo savings desc (then instance count desc) so the highest-impact fixes float to the top - Report Generation:
Mismatched PBR Mapsvalue cell now also shows the GPU-memory savings from downscaling each mismatched combo's bigger-than-color slots to match its color map. Computed via the engine allocation model with proper asset deduplication: a single oversized texture shared across many combos counts once, not once per usage; a shared asset still needed at full size by any matched combo correctly contributes zero - Scan: result table now lives under an
Assetssub-tab. The asset table itself is unchanged — every row keeps showing the literal authored size and per-asset GPU footprint
Full Changelog: v1.4.2...v1.5.0