LuxImage is a demo application of LUX.Data.Image, an ultra-high-resolution image library for Delphi / FireMonkey. Because TLuxImage keeps every pixel in CPU memory as 256 × 256 tiles with a built-in mip pyramid, image size is not bounded by the GPU texture limit: it is unlimited as far as RAM allows. The application opens PNG / JPEG files, and renders a Mandelbrot set of up to 65,536² pixels on all cores while showing the blocks as they finish.
- LUX :The LUXOPHIA standard library, whose
LUX.Data.Imageunits provide the tiled ultra-high-resolution image classes, viewer, and asynchronous file I/O used by this application.
FireMonkey's TBitmap shares its storage with the GPU and therefore inherits the GPU's texture size limit, which in practice caps it at about 8,192 × 8,192 pixels. TLuxImage stores pixels purely in CPU memory instead, and its viewer TLuxImageViewer hands only the visible tiles to Skia — so the per-frame cost depends on the size of the window, not the size of the image.
Main features demonstrated by this application:
-
No GPU size limit. Pixels live in CPU memory, in 256 × 256 tiles (
LUXIMAGE_TILE = 256), each tile a separate heap block. Every tile of every level is allocated bySetSize; an image that does not fit in free physical memory is refused there and then withEOutOfMemory. -
Parallel rendering with live display.
TLuxImageWorkercuts the image into 64 × 64 blocks and hands them one at a time to a thread per logical processor; each finished block is reported withTileChanged, and the viewer — which validates its tile cache against per-tile stamps and updates only the footprints of changed tiles in the pyramid — shows it within a frame. The Mandelbrot set is the demo's stand-in for ray tracing: pixels inside the set run to the iteration limit while pixels outside escape in a few steps, so per-block cost varies by orders of magnitude and only dynamic block assignment keeps the threads balanced. -
Four pixel formats, each mapping one-to-one onto a native Skia color type, so tiles reach the GPU without any pixel-format conversion:
Class Pixel record Bytes / pixel Skia color type Default display gamma TLuxImageUInt08TByteRGBA4 BGRA88881.0 TLuxImageUInt16TWordRGBA8 RGBA161616161.0 TLuxImageSFlo16THalfRGBA8 RGBAF162.2 TLuxImageSFlo32TSingleRGBA16 RGBAF322.2 Integer formats are taken to hold display-encoded (sRGB) values, so their default gamma is 1.0; floating-point formats are taken to hold linear values, so their default gamma is 2.2 and tone mapping is enabled by default. Alpha is stored straight (not premultiplied).
-
Built-in mip pyramid [2][3], allocated with the image and rebuilt incrementally — a changed tile costs a third of a tile to propagate, a loaded file a third of the image, both in parallel — so a fully zoomed-out view costs no more than a zoomed-in one, however large the image.
-
Asynchronous file I/O — loading and saving run on a worker thread (
TTask) with per-row progress reporting and completion events delivered to the main thread throughTThread.Queue, so the window stays responsive however large the file. PNG (implemented directly onSystem.ZLib, all bit depths / color types /tRNS/ Adam7) and JPEG (via the Skia codec) are supported both ways. -
GPU tone mapping and gamma correction as an SkSL runtime color filter [1][4], so changing the display settings costs nothing and does not invalidate the tile cache.
-
Colour management through
LUX.Color.Space: an image may carry a colour space (sRGB, Display P3, Adobe RGB, Rec.2020, ProPhoto RGB, ACEScg and their linear forms, or a custom one), which is embedded in PNG (sRGB/iCCP) and JPEG (APP2 ICC) on save and recovered on load. The viewer converts on the GPU — image transfer function → primaries matrix → display transfer function — to the ICC profile Windows assigns to the monitor the window is on, or to a space you set. Pixel values are never modified.
The viewer draws from the pyramid level
where
Level 0 is the original
i.e. the pyramid adds roughly 33 % to the memory of the base image [3], and SetSize allocates all of it up front.
Every level-0 tile carries a dirty flag, and every tile of every level a stamp that advances when its content changes. A thread that finishes writing a tile calls TileChanged, two atomic operations and no lock. Before each frame the viewer calls UpdateLevels, which rebuilds only the footprint of each dirty tile in the levels above — a square of side
of a tile, however large the image; a full load marks every tile dirty and goes through the same path.
TLuxImageWorker numbers the 64 × 64 blocks in raster order and hands them to Notify and OnProgress to about 30 Hz.
Gamma correction is applied per channel on the GPU:
with ガンマ slider (default 1.0 for integer formats, 2.2 for floating-point ones).
Tone mapping is the extended Reinhard operator [1], applied per RGB channel to the un-premultiplied color White, default 1):
For トーンマップ or dragging ガンマ never touches the CPU-side tile cache.
■ Ownership
・TForm1 (Main.pas) ・・・ UI: format combo, gamma, render size, progress
┣・TLuxImage (abstract) ・・・ owns
┣・TLuxImageWorker ・・・ owns; renders the Mandelbrot set into the image
┗・TLuxImageViewer :TFrame ・・・ Image (LUX.Data.Image.Viewer.pas)
■ Class hierarchy — pixel formats (storage: 256×256 tiles + mip pyramid, all allocated by SetSize)
・TLuxImage (abstract)
┣・TLuxImageUInt08
┣・TLuxImageUInt16
┣・TLuxImageSFlo16
┗・TLuxImageSFlo32
■ Parallel rendering and change tracking
・TLuxImageWorker
┗・N threads, one 64×64 block per atomic fetch
┗・TForm1.Mandelbrot( ThreadI, X,Y,W,H ) ・・・ SetRow per row of the block
┗・TLuxImage.TileChanged( TX,TY ) ・・・ Dirty := 1, Stamp++ (atomic, no lock)
┗・TLuxImage.Notify ( ≤ 30 Hz ) ・・・ OnChange → TLuxImageViewer.Redraw
┗・OnProgress ( ≤ 30 Hz ), OnFinished
■ Per-frame draw pipeline
・TLuxImageViewer
┗・per frame
┗・1. UpdateLevels: footprints of dirty tiles into levels 1…, in parallel
┗・2. pick level l with eq. (1)
┗・3. enumerate visible tiles (≈ 54 for a 1920×1080 window)
┗・4. TileImage(): validate by stamps of tile + 8 neighbours, else gather 1-px apron, wrap as ISkImage
┣・cache in TDictionary<TTileKey,TTileImg> (CACHE_MAX 512, LRU)
┗・5. ACanvas.DrawImageRect per tile
┣・ISkRuntimeEffect color filter (tone map + gamma, SkSL)
┗・ISkCanvas: FMX Skia canvas
┣・Vulkan GPU backend when available
┗・falls back to an intermediate raster TBitmap otherwise
■ Asynchronous file I/O
・TLuxImage
┗・LoadFromFileAsync / SaveToFileAsync ・・・ (LUX.Data.Image.Files.pas)
┗・TTask worker
┣・decode/encode PNG (System.ZLib) or JPEG (Skia codec)
┣・build the whole mip pyramid in parallel ( UpdateLevels with every tile dirty )
┗・OnProgress / OnLoaded / OnSaved queued to the main thread
The viewer draws nothing while Busy is set, because a load begins with SetSize, which replaces the tile structure; a render, by contrast, writes into tiles that already exist, so the viewer keeps drawing throughout and only ever reads a tile after its dirty flag has been taken down. Cached tile images carry a one-pixel apron gathered from the neighbouring tiles, so linear filtering at a tile boundary reads real neighbouring pixels and no seams appear.
・LuxImage/
┣・LuxImage.dpr ・・・ GlobalUseSkia := True (Vulkan)
┣・LuxImage.dproj ・・・ RAD Studio project (Win64)
┣・Main.pas / Main.fmx ・・・ TForm1: panel, progress, info
┣・_LIBRARY/
┃ ┗・LUXOPHIA/LUX/ ・・・ git subtree of LUXOPHIA/LUX
┃ ┣・LUX.pas ・・・ base declarations
┃ ┣・Color/ ・・・ pixel types (TByteRGBA, …), colour spaces (LUX.Color.Space)
┃ ┣・D1/Half/ ・・・ THalf, the half-precision scalar
┃ ┗・Data/Image/
┃ ┣・LUX.Data.Image.pas ・・・ TLuxImage: tiles & pyramid, change tracking
┃ ┣・LUX.Data.Image.Files.pas ・・・ TLuxImageFiler: format base class + registry
┃ ┣・LUX.Data.Image.Files.Png.pas ・・・ TLuxImageFilerPng ( Alpha, Level )
┃ ┣・LUX.Data.Image.Files.Jpg.pas ・・・ TLuxImageFilerJpg ( Quality )
┃ ┣・LUX.Data.Image.Worker.pas ・・・ TLuxImageWorker: parallel block scheduler
┃ ┣・LUX.Data.Image.Viewer.pas ・・・ TLuxImageViewer
┃ ┗・README.md ・・・ full library documentation
┗・--------/_SCREENSHOT/LuxImage.png
LUX.Data.Image.pas and LUX.Data.Image.Worker.pas use neither FireMonkey nor Skia; those dependencies are confined to the file and viewer units. Full library documentation: _LIBRARY/LUXOPHIA/LUX/Data/Image.
The application opens with nothing loaded — pick an image with 開く…, or render one with 描画開始. No image is bundled; anything sized up to tens of thousands of pixels square will show what the library is for.
| Control | Action |
|---|---|
| Mouse wheel (towards you) | Zoom in around the cursor; four notches double the scale |
| Left drag | Scroll |
開く… (Open) |
Open a PNG / JPEG asynchronously into the selected pixel format |
保存… (Save) |
Save asynchronously through the filer for the chosen extension — PNG at the selected compression level, JPEG at quality 90 |
PNG 圧縮率 (PNG compression) |
非圧縮(最速) / 速さ優先 / 既定 / 小さ優先(最遅). PNG is lossless, so this changes only file size and write time, never the pixels |
画素形式 (Pixel format) |
Format the next-opened or next-rendered image: UInt08 / UInt16 / SFlo16 / SFlo32
|
全体表示 (Fit) |
Fit the whole image to the window |
等倍 ( 1 : 1 ) (1:1) |
One image pixel per screen pixel |
ガンマ (Gamma) |
Display gamma |
トーンマップ (Tone map) |
Reinhard tone mapping, eq. (4); on by default for floating-point formats |
色空間 (Colour space) |
The image's colour space — なし (none) or one of the ten presets, sRGB to ACEScg. A loaded file's embedded profile selects the matching entry (or adds its own); the choice is embedded when saving and used for a render. With a colour space the viewer converts to the monitor's profile on the GPU; the info label shows image → display
|
並列描画(マンデルブロ集合) (Parallel render) |
Side of the square image to render: 4,096 … 65,536 pixels |
描画開始 / 中止 (Render / Cancel) |
Allocate an image of that size in the selected format and render the Mandelbrot set on all cores; the view fills in block by block and can be zoomed and scrolled meanwhile. Pressing again cancels after the blocks in flight |
The label at the bottom of the panel reports the image size, the pixel format, the current zoom, and which pyramid level is being drawn; load/save/render times are shown for a few seconds after each operation, and the percentage and elapsed time during a render.
If the selected size does not fit in free physical memory the render is refused with a message before anything is allocated: 65,536² pixels take 16 GB in UInt08 and 64 GB in SFlo32, plus a third for the pyramid.
-
RAD Studio (Delphi) with FireMonkey + Skia [5], both of which ship with RAD Studio.
-
The project source enables the Skia canvas and its Vulkan backend:
uses ..., FMX.Skia, FMX.Skia.Canvas.Vulkan, ...; GlobalUseSkia := True; GlobalUseSkiaRasterWhenAvailable := False; // do not fall back to the CPU raster canvas
With this in place the viewer draws straight into the window surface. Without it the viewer still works, through an intermediate raster bitmap, at the cost of a full-window blit every frame.
-
Open
LuxImage.dprojand build for Win64. -
sk4d.dllmust sit next to the executable; RAD Studio deploys it automatically when the project is built from the IDE.
- E. Reinhard, M. Stark, P. Shirley, J. Ferwerda, "Photographic Tone Reproduction for Digital Images", ACM Transactions on Graphics (Proc. SIGGRAPH), 21(3), 2002.
- L. Williams, "Pyramidal Parametrics", Computer Graphics (Proc. SIGGRAPH), 17(3), 1983.
- Mipmap — Wikipedia.
- SkSL — Skia Shading Language.
- Skia4Delphi.
Integrated Development Environment (IDE) for Creating Native Cross-Platform Apps.
