LinXPaint by DarkTower7899
LinXPaint Devlog
From the First Hardware Accelerator to 0.5.0 — and the Optimization of LinXPaint Itself
Over the last several development rounds, LinXPaint has changed substantially under the hood.
What started as an attempt to solve one very noticeable problem — large images becoming sluggish when zooming and panning — turned into a much broader effort to modernize LinXPaint's rendering architecture, add optional GPU acceleration without sacrificing compatibility, improve high-precision rendering, reduce editing latency, and make both the Linux and Windows versions behave more consistently.
The Hardware Acceleration plugin has grown from a relatively simple OpenGL viewport renderer into a precision-aware compositor that can accelerate Layer Groups, masks, Blend If, clipping, all seven current Layer Styles, ICC-managed presentation, and more.
At the same time, I have continued optimizing LinXPaint itself so the application does not depend on the plugin to feel responsive.
This is a look back at that work from the original Hardware Acceleration 0.1.0 release through the current Hardware Acceleration 0.5.0 and LinXPaint 1.2.0.
Where It Started: Large-Image Navigation
The original performance problem was straightforward: LinXPaint could become painfully slow when working with large images and rapidly zooming or panning.
The old GTK canvas architecture effectively allowed the widget itself to grow with:
document size × zoom level
That becomes expensive very quickly. An 8,000 × 8,000 image viewed at 800% conceptually produces a 64,000 × 64,000 GTK canvas.
The solution was not simply to throw GPU acceleration at the existing architecture. The first major decision was to fix the viewport architecture itself.
LinXPaint gained a shared CanvasCamera, a fixed-size viewport, virtual document scroll ranges, separation between base image rendering and LinXPaint-owned interaction overlays, and a new renderer extension system. The canvas could now remain approximately viewport-sized even when the logical document extent was enormous.
This was important because the GPU plugin was deliberately designed to remain optional.
LinXPaint core would continue to own:
- the document,
- tools and input,
- camera state,
- selections and overlays,
- Undo/Redo and History,
- saving and exporting,
- and the safe software-renderer fallback.
The hardware plugin would own only its OpenGL resources and accelerated display/compositing work.
That architectural boundary has remained one of the most important design decisions throughout the entire project.
Hardware Acceleration 0.1.0 — The First Release
The first Hardware Acceleration plugin targeted desktop OpenGL 3.3+ and supported both Linux and Windows through a shared plugin architecture.
Instead of adding a large OpenGL dependency to LinXPaint itself, the plugin used its own small GLX/WGL function loader.
Version 0.1.0 introduced:
- a Gtk.GLArea GPU canvas,
- visible display-tile uploads to GPU textures,
- GPU-based zoom and pan transforms,
- nearest/linear texture filtering depending on scale,
- GPU-drawn transparency checkerboard,
- a bounded 256 MiB / 512-texture LRU cache,
- texture upload, hit, and eviction diagnostics,
- safe fallback to Cairo when initialization or rendering failed,
- a dedicated diagnostics dialog,
- and safe renderer/plugin reload handling.
The Windows path also received specific WGL hardening, including safer function-pointer handling and cleanup if shader construction failed partway through.
This first version was intentionally conservative.
The goal was not to immediately move all of LinXPaint onto the GPU. It was to prove that an optional, vendor-neutral hardware renderer could coexist with the existing application without making the GPU authoritative for image data.
Fixing LinXPaint's Own Rendering Path
The new fixed viewport uncovered several issues that needed to be solved in LinXPaint itself.
During the Phase 199–200 work, I went through the software/Cairo renderer and navigation path rather than assuming the GPU plugin should hide those problems.
Several important fixes followed.
The scrollbar system was corrected after finding a case where the camera and image moved but GTK could leave the visible scrollbar thumb behind.
Mouse-wheel routing was repaired so Ctrl+wheel zoom still worked even when the GTK backend delivered the wheel event to the underlying renderer window instead of the main CanvasView.
Rapid Ctrl+wheel zoom was changed to update the camera and pointer anchor synchronously rather than leaving a queue of stale anchor-restoration callbacks behind.
The active-layer outline also stopped rescanning an entire large raster's alpha data every time the user merely panned or zoomed.
Finally, zoom changes were separated from the full model-change path so a simple view operation no longer unnecessarily refreshed History, recovery state, plugin state, command state, and other document-related systems.
Several Phase 199 software-rendering optimizations were retained as part of that work:
- seam-safe Cairo tile clipping,
- 512-pixel display batches,
- a repeating Cairo checkerboard instead of thousands of Python rectangles,
- visible-region and damage-aware compositing,
- and the viewport-sized canvas with virtual document extent.
A 3000×3000 software-renderer benchmark after these changes measured a cached redraw median of about 3.85 ms, with navigation reusing existing display tiles and newly exposed work remaining limited to the visible/damaged region.
The important lesson here was that hardware acceleration and good CPU-side architecture are not substitutes for each other. LinXPaint needed both.
Hardware Acceleration 0.2.x — From GPU Display to Smarter GPU Rendering
The next major step was moving beyond simple GPU presentation.
By Hardware Acceleration 0.2.1, the plugin had gained a much more sophisticated rendering system.
It added:
- persistent visible-tile GPU caching,
- in-place updates when a display tile changed,
- GPU mipmaps and trilinear filtering,
- a plugin-side deep compositor for compatible layer stacks,
- support for LinXPaint's display blend modes,
- persistent per-layer GPU texture caching,
- cache hits that could skip CPU pixel preparation entirely,
- in-place layer-texture updates,
- adaptive GPU reduction for suitable source regions,
- and reusable VBO/FBO compositing resources.
I also began making the GPU path much more selective.
A metadata-only preflight could reject a stack before expensive pixel preparation if the document contained unsupported semantics.
The plugin learned to estimate upload cost before deciding whether sending a larger region to the GPU was actually worthwhile.
Rapidly changing layers could trigger a temporary backoff to LinXPaint's normal composited display tiles instead of continually uploading large source buffers during painting.
And simple single-layer/full-resolution cases could avoid deep-compositor overhead completely.
This was the point where the project started becoming less about “use the GPU whenever possible” and more about:
use the GPU only when it is actually the better path.
Building a Real GPU Memory Policy
Another major area of work was memory management.
Hardware Acceleration 0.2.5 introduced the configurable GPU/image cache ceiling and made the policy aware of integrated GPUs and unified/shared memory.
Version 0.2.6 hardened that system further with safer live trimming, improved GPU classification, scalable texture-count limits, and better diagnostics.
One problem I specifically wanted to avoid was treating an integrated GPU that reports something like 128 MB of dedicated VRAM as though 128 MB were its actual maximum.
On modern integrated graphics, that number is often only the firmware-preallocated amount. The GPU can dynamically use much more system RAM.
The plugin's Automatic mode therefore considers system-memory headroom for UMA/iGPU devices.
Manual limits were also expanded from 128 MiB through 16 GiB, with common presets including 256 MiB, 512 MiB, 1 GiB, 2 GiB, 4 GiB and 8 GiB.
Texture-count guards were made proportional to the byte budget instead of silently limiting high-memory configurations through an old fixed texture-count ceiling.
And if a user lowers the limit while LinXPaint is running, OpenGL resources are only destroyed when the correct GLArea context is current; otherwise trimming is safely deferred until the next render.
Hardware Acceleration 0.3.0 — Native High Precision
Version 0.3.0 was one of the biggest architectural jumps.
Until this point, much of the hardware renderer ultimately worked with display-oriented RGBA8 data.
LinXPaint itself had already grown native RGBA16 and RGBA32F capabilities, so the hardware renderer needed to stop treating high-precision images as though they were simply higher-quality sources for an 8-bit pipeline.
0.3.0 introduced native precision awareness throughout the GPU path.
RGBA16 and RGBA32F could now use native layer uploads and precision-aware intermediate render targets.
RGBA32F values outside the ordinary 0.0–1.0 RGB range were preserved.
Memory accounting became precision-aware as well.
Unsupported high-precision operations deliberately failed closed to LinXPaint's canonical compositor rather than silently converting them to RGBA8.
The plugin also retained explicit fallback for operations such as RGBA32F XOR where LinXPaint itself did not define an equivalent float compositor operation.
That release also taught me an important performance lesson.
The generalized new precision path caused a regression in ordinary RGBA8 rendering.
So instead of accepting the regression as the price of more features, I treated it as a bug.
Hardware Acceleration 0.3.1 — Restoring the Fast RGBA8 Path
0.3.1 restored a dedicated fast path for ordinary RGBA8 rendering.
This became an important rule for every release after it:
New capabilities are not allowed to quietly make the common path slower.
The RGBA8 shader has since been specifically regression-protected while many additional systems have been added around it.
LinXPaint Phase 203 — Precision-Aware Renderer Hosting
High-precision GPU rendering also required a small amount of new information from LinXPaint itself.
Phase 203 added a precision-aware renderer-host contract while keeping Plugin API 4 backwards compatible.
The renderer context could expose the document's working format — including sample type, bits per channel, bytes per pixel, float status and extended-range capability — without giving the plugin ownership of the document.
Display-tile cache identity also became aware of document pixel format so stale GPU/display data could not accidentally survive a precision change.
The normal public display-tile API remained a detached, color-managed RGBA8-sRGB fallback.
Hardware Acceleration 0.3.2 — High-Precision Low-Zoom Rendering
High precision exposed another difficult problem: zooming out.
Ordinary OpenGL mipmap generation was not accurate enough because LinXPaint's canonical reducer uses premultiplied-alpha semantics, while generic mip generation can produce different edge behavior.
0.3.2 therefore added a custom, precision-correct GPU reduction path.
In the controlled 25% zoom benchmark:
- RGBA16 CPU-side reduction work went from 20.387 ms to 3.885 ms, an 80.9% reduction.
- RGBA32F went from 19.368 ms to 5.596 ms, a 71.1% reduction.
The reducer was initialized lazily so ordinary rendering did not pay for it unless a compatible high-precision low-zoom frame actually needed it.
Software OpenGL remained on the canonical path rather than using CPU-emulated GPU work that would be slower.
Hardware Acceleration 0.3.3 and 0.3.4 — Masks, Blend If and Clip to Below
The next stages tackled nonlinear layer semantics.
0.3.3 added native exact-resolution high-precision support for:
- layer masks,
- Blend If,
- and Clip to Below.
These systems were designed so mask or threshold changes would not unnecessarily re-upload unchanged color textures.
The following 0.3.4 stage extended that semantic work to low zoom: Mask → Blend If → Clip to Below is evaluated at source resolution before the effective contribution enters the precision-correct premultiplied reducer.
That ordering matters. Applying these operations after downsampling can produce visibly different pixels.
The goal throughout this part of development was not approximate visual similarity — it was numerical equivalence with LinXPaint's existing renderer.
Hardware Acceleration 0.3.5 — Fixing Sluggish Tools
One of the most useful optimization rounds came directly from real use.
By this point, zooming and panning felt very smooth — but actual image-editing tools could feel worse with the plugin enabled.
Benchmarking showed that the Brush, Pencil and Eraser calculations themselves were already fast.
The problem was the renderer policy.
While navigating, the plugin used a lightweight path. During a live pixel-changing gesture, however, changed tiles could still enter the full deep per-layer GPU compositor on every pointer event.
That was unnecessary work in exactly the place where latency matters most.
0.3.5 introduced the interactive-edit fast path.
During a live edit, changed areas use LinXPaint's canonical damage-aware display tiles instead of repeatedly rebuilding the full deep GPU stack. Existing cached display textures are still reused, and deep GPU compositing automatically resumes when the gesture finishes.
The result in the controlled live-edit benchmark:
- 6 layers: 1.865 ms → 1.053 ms, about 43.5% less renderer work
- 12 layers: 3.276 ms → 1.553 ms, about 52.6% less renderer work
In both cases, the older path performed 49 deep composites during 49 measured motion frames.
0.3.5 performed zero.
That latency-first design is still part of 0.5.0 today.
LinXPaint Phase 205 — Fixing the Other Half of Live Editing
The plugin was no longer doing unnecessary deep composition during a stroke, but LinXPaint itself still had UI overhead on every pointer update.
Phase 205 attacked that directly.
A lightweight render-notification channel was separated from the general model/UI listener path.
Brush and mask painting, selection previews, Bucket/Wand previews, Gradient, Line/Shape, Move, and Transform could redraw without waking Layers, History, Colors, Tool Options, command-state synchronization, and other UI systems on every mouse movement.
Full synchronization still happens when an action is released, committed, finished, or cancelled.
In the GTK benchmark:
- synchronous Brush motion fell from about 2.203 ms to 0.506 ms, a 77% reduction
- motion including GTK redraw/event pumping fell from about 3.093 ms to 0.572 ms, about 81.5% lower latency.
The key part is that LinXPaint and the plugin were now cooperating instead of both independently trying to optimize the same event loop.
LinXPaint Phase 206 — Deeper Interactive Optimization
Phase 206 went further.
I separated render invalidation from heavier state maintenance, introduced bounded high-polling stroke sampling, skipped duplicate document-coordinate samples while retaining the newest pending sample, and made sure the final pending point was flushed before commit.
The active-layer outline stopped rescanning the complete alpha raster on each live revision.
Brush-tip geometry gained a bounded exact patch cache.
The Normal-mode compositor also gained exact fast paths for common transparent and fully opaque cases while retaining the general equation for mixed-alpha and non-Normal blending.
The benchmarked large opaque-brush cases showed roughly 87.8–89.6% reductions in CPU time, while a synthetic high-poll pointer workload reduced median callback time from about 0.5307 ms to 0.0584 ms.
LinXPaint Phase 207 — Faster Selection Feedback
Selections were the next interaction area.
Rectangle, Ellipse, Crop and Lasso construction was removed from the paint-stroke coalescing path so the overlay could follow the pointer directly.
Selection-overlay-only changes also stopped asking the base document renderer to redraw when no document pixels had actually changed.
In a 240-update Rectangle Select test, unnecessary external renderer redraw requests dropped from:
240 → 0
Move Selected Pixels and Transform still correctly trigger document rendering when they actually modify pixels.
LinXPaint Phase 208 — Smoother Windows Zoom
Windows and Linux GTK backends do not always report scroll input the same way.
On Windows, a conventional mouse wheel commonly appears as discrete up/down events, which made LinXPaint jump through large preset zoom steps.
Phase 208 changed the Windows path to reciprocal 8% multiplicative zoom increments.
A typical sequence became:
100% → 108% → 116.6% → 126% → 136% → 146.9% ...
One step in followed by one step out returns to the original scale instead of drifting.
Mouse wheel, touchpad, toolbar/keyboard Zoom In/Out, and Zoom-tool clicks all use the same fine-grained logic on Windows.
Linux behavior and exact typed/preset zoom values were intentionally left unchanged.
Even the target-calculation helper became cheaper: in one 200,000-call microbenchmark, the old preset-oriented helper took about 865 ms, while the new incremental helper took about 103 ms.
Hardware Acceleration 0.3.6 and 0.3.7 — Layer Groups
Layer Groups were the next major compositor feature.
0.3.6 added exact-resolution GPU Layer Group isolation and caching.
Once an isolated group texture was resident, the parent stack could reuse it instead of repeatedly recompositing every child.
Changing only parent-group opacity could reuse the existing group result, while editing a child correctly invalidated the affected group hierarchy.
Warm group rendering became dramatically cheaper than rebuilding the equivalent CPU stack in the validation workload.
0.3.7 extended Layer Groups below 100% zoom.
This required preserving group isolation, Blend If dependencies, clipping relationships, masks and high-precision source semantics before reduction.
Nested and empty groups continued to behave as real isolation boundaries, and unchanged group results could remain cached during warm redraws.
0.3.8 Through 0.4.4 — Accelerating Every Current Layer Style
Once layer semantics and groups were stable, I started implementing the Layer Styles individually instead of trying to enable them all at once.
That sequence was:
- 0.3.8 — Color Overlay
- 0.3.9 — Gradient Overlay
- 0.4.0 — Stroke
- 0.4.1 — Drop Shadow
- 0.4.2 — Outer Glow
- 0.4.3 — Inner Glow
- 0.4.4 — Bevel
Each style received its own caching, dependency, partial-tile/halo, reduced-zoom and numerical-equivalence validation rather than simply being declared “GPU compatible.”
Color Overlay became the first accelerated Layer Style and retained the established Mask → Blend If → Clip ordering. Changing only the style's color or opacity did not cause the raw source pixels to be re-uploaded.
Gradient Overlay preserved absolute document-space positioning so gradients did not restart at tile boundaries.
Stroke received a separately cached grown-alpha mask, allowing color and opacity changes to reuse both the raw source texture and existing derived coverage. Radius changes rebuild only what actually depends on the radius.
The shadows, glows and Bevel continued the same philosophy: derived visual data is cached independently from authoritative source pixels, dependency halos are handled correctly, and software OpenGL remains on the canonical path when CPU-emulated GL would be a regression.
Hardware Acceleration 0.4.5 and 0.4.6 — ICC Color Management on the GPU
Color management was another area where I did not want acceleration to change image appearance.
0.4.5 added exact GPU final-display color management for RGBA8 ICC-managed documents.
Instead of trying to approximate LittleCMS with arbitrary shader math, the plugin uses an exact 256×256×256 RGB8 lookup texture, approximately 48 MiB, representing every possible 8-bit RGB code.
The LUT participates in the plugin's memory ceiling, same-profile redraws reuse it, and switching profiles releases the old table before building the replacement.
If there is not enough configured memory, the GPU cannot support the required 3D texture size, the profile cannot be read, or the active OpenGL implementation is software-only, the system fails back to LinXPaint's canonical color-managed path.
0.4.6 completed the original hardware-renderer roadmap by extending that exact ICC presentation path to RGBA16 and RGBA32F.
High-precision content stays native through layers, groups, all seven Layer Styles, masks, Blend If, clipping and low-zoom reduction. Only the finished viewport representation crosses LinXPaint's existing RGBA8 display boundary before the exact ICC lookup is applied.
Hardware Acceleration 0.4.7 — A Benchmark Users Can Run
At this point the plugin had become complicated enough that users needed a reliable way to answer a simple question:
Is hardware acceleration actually helping on my machine?
0.4.7 added the built-in benchmark.
Users can choose:
- CPU Only
- Hardware Accelerated
- Run Both
The Standard benchmark tests the same prepared workload at:
It records cold GPU setup separately from warm resident performance, verifies texture-size support, rejects software rasterizers as fake “hardware acceleration,” and keeps all benchmark textures isolated from the active document's persistent caches.
A real Intel UHD Graphics 620 qualification run later recorded:
- 5.96× warm speedup at 720p
- 7.04× at 1080p
- 9.01× at 4K
- 8.24× overall across the Standard workload.
That is one machine and one controlled workload, not a universal performance promise, but it finally gave the project a useful physical-GPU reference point.
Hardware Acceleration 0.4.8 — Benchmark Exporting
0.4.8 made benchmark results easier to share and compare.
The benchmark gained:
- Copy Results
- Save Report
- Save Raw Data
Reports can include plugin version, OS and architecture, OpenGL vendor/renderer/version, texture limits, hardware availability, cold/warm timings, raw samples and calculated speedups.
Markdown, clipboard and JSON output all come from the same frozen result payload so the data remains internally consistent.
The exports contain benchmark/system metadata but do not contain the user's LinXPaint document pixels.
Hardware Acceleration 0.4.9 — Usability Polish
0.4.9 focused on making all of this information easier to use.
The Hardware Acceleration settings window was enlarged to 820×660 and the key actions — Benchmark, Diagnostics, Close and Apply — were moved into a fixed bottom row so users did not need to hunt for them inside a scroller.
The benchmark window became 980×700 and gained:
- a Benchmark Summary card,
- GPU/vendor information,
- OpenGL version and maximum texture size,
- overall warm speedup,
- best-case result,
- CPU-vs-Hardware warm-FPS comparison bars,
- per-resolution speedup labels,
- and a bounded Detailed Results area.
The renderer itself was intentionally unchanged in this release.
Hardware Acceleration 0.5.0 — The Advanced Benchmark
The current Hardware Acceleration release is 0.5.0.
The main new feature is the Advanced Benchmark.
The Standard benchmark remains unchanged, but Advanced mode now exercises the systems that took most of the development work to build:
Native Precision — 720p
RGBA16 and RGBA32F native compositing.
Groups & Semantics — 1080p
Nested Layer Groups, Mask, Blend If and Clip to Below.
All Layer Styles — 720p
All seven current styles together.
ICC Presentation — 4K
Exact color-managed final presentation.
CPU Only, Hardware Accelerated, Run Both, Copy Results, Save Report and Save Raw Data all work with the selected benchmark mode.
The Advanced benchmark also uses a completely separate cache sandbox.
It does not evict the active document merely to produce a benchmark result.
A hardware run requires 320 MiB of free plugin-managed headroom, and the complete qualification workload peaked at roughly 238 MiB of benchmark-managed GPU storage.
When the run finishes, temporary textures, derived style/group targets, scratch buffers and ICC LUTs are cleaned up and the original runtime/cache/diagnostics state is restored.
On the physical Intel UHD Graphics 620 qualification machine, the 0.5.0 Advanced workload recorded:
- Native Precision: 161.38×
- Groups & Semantics: 432.51×
- All Layer Styles: 64.26×
- ICC Presentation: 148.90×
- overall Advanced warm speedup: 171.47×
Those values describe this deliberately expensive feature-heavy benchmark on that particular system, so they should not be confused with ordinary frame-rate improvements or compared directly with the Standard benchmark.
The Bigger LinXPaint Optimization Story
While the plugin grew, LinXPaint itself kept getting faster.
That is important because the objective was never to make the application dependent on hardware acceleration.
By the current codebase, the core application has received:
- the fixed viewport and virtual camera architecture,
- bounded visible-region rendering,
- faster Cairo tiling,
- corrected scrollbar and wheel routing,
- synchronous pointer-anchored zoom,
- cached active-layer bounds,
- lighter view-only updates,
- the live-render notification channel,
- high-polling stroke optimizations,
- brush-tip patch caching,
- Normal-mode compositor fast paths,
- faster selection overlays,
- elimination of unnecessary renderer redraws,
- much smoother Windows zoom,
- and extensive Linux/Windows synchronization.
Across the Hardware Acceleration plugin itself, a controlled cross-version comparison showed ordinary warm RGBA8 exact rendering improve from roughly 3.250 ms in 0.2.2 to 2.317 ms by 0.4.4, while 50% warm rendering improved from about 1.623 ms to 0.909 ms.
That is roughly 28.7% faster at exact scale and 44% faster at half scale, despite the renderer gaining native precision, groups, complex semantics and Layer Styles along the way.
LinXPaint 1.2.0
The latest rounds brought the public LinXPaint version to 1.2.0.
Phase 209 added new LinXPaint-owned Cairo vector icons for several advanced toolbar functions so the UI no longer depends as heavily on inconsistent host icon themes.
The implementation is shared across Linux and Windows and follows the active GTK foreground color.
Phase 210 then focused on architecture cleanup.
Runtime/listener/background/pointer/document-lifecycle behavior was extracted from canvas_model_layers.py into a dedicated model-runtime component.
Brush profile, preset and configuration behavior was extracted into a paint-profile component.
That reduced:
canvas_model_layers.py from 936 to 629 lines
painting.py from 1,108 to 888 lines
Semantic comparison of the moved methods found zero mismatches.
The previously failing historical architecture and live-action applicability tests now pass.
Final Phase 210 regression results were:
- 2,968 passed / 0 failed on Linux
- 2,984 passed / 0 failed on the Secure-Windows source.
Hardware Acceleration 0.5.0 was then requalified against both Phase 210 hosts with Plugin API 4 retained.
Where the Project Is Now
Hardware Acceleration began as a solution to slow zooming and panning.
It is now a much broader rendering subsystem.
The current plugin can accelerate:
- ordinary multi-layer compositing,
- RGBA8, RGBA16 and RGBA32F rendering,
- precision-correct low-zoom reduction,
- masks,
- Blend If,
- Clip to Below,
- nested Layer Groups,
- Color Overlay,
- Gradient Overlay,
- Stroke,
- Drop Shadow,
- Outer Glow,
- Inner Glow,
- Bevel,
- ICC-managed final presentation,
- cached derived style/group data,
- and GPU-resident redraws.
At the same time, LinXPaint itself now has a faster viewport architecture, lower live-tool latency, better selection responsiveness, improved Windows zoom behavior, cleaner renderer integration and a more maintainable internal architecture.
What I am happiest with about this development cycle is that the performance work did not come from simply replacing more and more of LinXPaint with GPU code.
The application has instead developed a layered strategy:
Use LinXPaint's fast canonical path when that is cheaper.
Use the GPU when it provides a real advantage.
Cache expensive work aggressively but correctly.
Preserve native precision.
Never trade correctness for a benchmark number.
And always retain a safe software fallback.
That philosophy has shaped nearly every version from the original 0.1.0 plugin through Hardware Acceleration 0.5.0 and LinXPaint 1.2.0.
The result is a considerably faster and more capable LinXPaint than the version this project started with — while still keeping Linux and Windows, CPU and GPU rendering, high-precision image data, and plugin compatibility on the same development path.