Sign in

NON-TOXICNo ads · No shady algorithms

iiinova is part of the Atmosphere — an open, public network where you own your identity.

Ignacio Castaño🍉

@castano.mastodon.gamedev.place.ap.brid.gy
80 followers 3 following 315 posts

Master swimmer, mountain climber, and stand up coder. I work on texture processing tools at Ludicon. Formerly at Roblox, Thekla (The Witness), NVIDIA […] 🌉 bridged from ⁂ mastodon.gamedev.place/@castano, follow @ap.brid.gy to interact

PostsRepliesMedia
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 25/10/2026
Yay! I just fixed a quality regression on PVR devices that had been bugging me for a long time. Turns out this code is particularly sensitive to the floating point rounding mode and most PVR devices round toward zero (RZ) instead nearest even (RNE). Series D appears to be the first RNE device. B […]
mastodon.gamedev.place
Original post on mastodon.gamedev.place
010 1
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 19/10/2026
Very pleased with the latest results of the ETC2 codec. I'm surprised Spark now scores better than all other codecs, except CVTT. It clearly falls behind on some some images, where you really need T/H modes and an exhaustive table selection. I'm […] [Original post on mastodon.gamedev.place]
000 0
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 15/10/2026
Revisiting the ETC codec. Tuning against @EricLasota's CVTT has proven useful to identify the largest gaps between what Spark achieves and what is possible in the format. Turns out I had some small bugs that were degrading quality. I also bisected the […] [Original post on mastodon.gamedev.place]
ssimu2 chart comparing Spark 1.4 vs Spark 1.5
000 0
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 13/10/2026
Tuning the Spark kernels for PS5 I was able to get up to a 59% performance improvement. I may need to put together another release soon!
020 2
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 10/10/2026
I've written a brief retrospective of the last 4 years, with my thoughts on middleware, AI, and texture compression. I consider this a first draft, would love to hear your feedback before sharing more widely. www.ludicon.com/castano/blog/2026/1…
ludicon.com
4 years of Ludicon
Last weekend marked the four-year anniversary of Ludicon. I started Ludicon because I believed that working independently would let me do my best work. Building software on my own, I can move fast, take risks, and focus on problems others don’t consider practical, or slow down and go deep on rabbit holes without having to justify myself. I’m happy with the quality of the work I’ve produced, excited about what is yet to come, but disappointed with its commercial success so far. In retrospect, 2022 was not a particularly good time to start a game middleware business. In that year alone the game industry saw 8,500 layoffs. That trend would continue getting worse and has not stopped yet. Middleware had been in a steady decline. Unity, Unreal and others offered complete engines with no upfront costs. The question was no longer whether a third-party component was better than what you’d write yourself, but whether it was better enough to justify replacing what the engine already provides. It rarely was. Epic’s acquisition of RAD in 2021 marked the end of that era: the last major independent vendor of core runtime technology was now part of an engine. By the time I started working on Spark in 2022, engine consolidation had won. Against that backdrop, the business case was ambiguous, but my hope was that with reduced headcounts, long-term research into core technology would slow down and outsourcing that technology would become more attractive. When I started working on Ludicon I believed that there would be a growing market for texture compression. Games were only getting larger, fidelity targets kept increasing, and textures were often the assets with the largest footprint. At the same time, an increasing amount of content was generated by players themselves. This created an explosion of content, and with it the need to compress at runtime. What I did not anticipate was AI. Generative tools lower the barriers to content creation. At the same time, the AI buildout has created shortages and driven up the price of memory and storage. Existing devices are staying in use longer, and new devices often ship with less memory than they did a few years ago. Both pressures push in the same direction: more content, less room for it. Over the last year AI has also changed the software industry. This transformation has been so fast that it induced vertigo. At some point, I wondered if my job would be at risk. Could LLMs produce competitive texture codecs on their own? I tried and they were nowhere close. Still, AI has transformed the way I code, but not the way you would expect. For the most part I still write the codecs by hand, the old fashioned way. AI just lets me do that more efficiently. AI reviews my changes and it routinely catches bugs that would have taken hours to debug. It has allowed me to build tools that feel like having superpowers. For example, I can edit one shader and automatically display statistics and annotated assembly listings on all the platforms that I support. When a customer reports a bug or issue, I just plug in the relevant device and let an agent build a repro and suggest a fix for the problem. When a CI build fails, an agent automatically fixes it and opens a PR. The validation, CI, and regression testing tools I have today are better than what I would have imagined possible as a solo developer. This has allowed me to stay focused on the problems that I enjoy working on and where I can make a difference. After Epic’s acquisition of RAD I though there would be a need for an independent technology provider, and I hoped I could fill that vacuum. Filling it with another RDO encoder meant competing with an established product built by some of the brightest developers I know. I had written RDO encoders before and I concluded it was a race I could not win on quality or performance. In all likelihood I would be forced to compete on price. Real-time compression was more interesting. It solved the problem of content created at runtime, and it opened a different solution space for storage and transmission. Traditional RDO block encoders are constrained by the fixed-rate block structure and do not scale to low bitrates. With real-time compression, the intermediate representation can be anything, as long as it can be encoded into a GPU-friendly format at load time. I still think this is the right direction. Modern image formats like WebP and AVIF, combined with Spark, already deliver much better compression ratios than RDO block encoders. However, they suffer from slow decompression performance. In the next year or so I predict there will be new codecs with competitive quality and much higher decoding throughput. The recent work on neural textures and learned codecs will widen the space further. Four years in, and I think my bet looks better than it did in 2022.
2102 10
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 09/10/2026
Something I worked on was released today. It looks great! The visuals haven't changed much since I left, but it seems to be holding up. I'm very proud of the work we did and sorry the team didn’t get to finish it together, all of us left when Thekla stopped being a good place to work.
160 6
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 09/10/2026
Ironically, the USPTO tried to warn me about this, but their email was classified as spam 🤦‍♂️
USPTO scam warning
000 0
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 08/10/2026
Well, I just filed a trademark application for Spark, which I guess I should have done ages ago. The moment I file it and my phone number goes into the public record I’m bombarded with scam calls. Yikes.
100 1
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 30/09/2026
Lowering occupancy is not always the answer. I was able to double throughput by artificially increasing occupancy in order to limit the number of threads that can run concurrently. In this case more threads just caused L1 thrashing. For best performance you have to size the dispatch so that the […]
mastodon.gamedev.place
Original post on mastodon.gamedev.place
000 0
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 25/09/2026
New blog post! Encoding render targets using Tile Shaders: www.ludicon.com/castano/blog/2026/0…
ludicon.com
Encoding Render Targets for Free with Tile Shaders
The _Spark_ codecs are often bandwidth-bound. The codec usually reads an uncompressed RGBA8 texture and outputs a block-compressed texture that, depending on the format, is 2–8 times smaller. Reading the input is what dominates bandwidth. When bandwidth-bound, execution stalls waiting for input data, while the GPU tries to hide that latency by overlapping loads for some blocks with computation on others. Mobile GPUs render one tile at a time and keep its data in on-chip memory to save bandwidth and power. Wouldn’t it be great if our codecs could also use this tile memory as their input? That’s exactly what Metal tile shaders allow us to do. When I started working on _Spark_ I was very excited about the possibilities of using tile shaders, but when I tried them on an iPhone 12 and M1 MacBook Pro the results were underwhelming. There seemed to be scheduling issues that caused the combined fragment + tile shader to run at a fraction of the speed of the individual shaders. The difficulties were understandable: On Apple GPUs a tile is typically 16×16 to 32×32 pixels, and to encode it we use a single thread for each 4×4 block. The tile dispatch has to wait for that tile’s fragment work to finish, and 16–64 threads is a small threadgroup to hide latency with. In most cases the fragment shader is relatively simple and has high occupancy, while the encoder is much more complex and tends to have lower occupancy. Scheduling that work effectively seemed quite challenging, so my initial excitement faded. However, a few months ago some Apple folks mentioned that the performance of tile shaders had improved in recent OS versions. I didn’t have high hopes, but I tested it again on a wider array of devices and to my surprise the results were much better. This could be attributed to many factors: improvements in the codecs, updated compiler, better testing methodology, different architectures, and OS updates. Today the tile path is 1.2–1.7× faster than a separate compute pass, and on most of the iPhones it’s as fast as or faster than writing to the uncompressed texture, essentially making real-time texture encoding free. The benefits of running the codecs in the tile shader do not end there. Lower bandwidth results in lower power consumption, and the uncompressed render target is never stored, so it can be memoryless, saving the allocation entirely. ## How to use tile shaders? In order to use the tile shader in Metal you need a device with at least an Apple4 GPU (A11) or later, running on iOS 11 or macOS 11. ### 1. The encode kernel Include the single-header _Spark_ codec and write a tile kernel. Each thread reads one 4×4 block from the `imageblock` and encodes it: #define SPK_ENABLE_FP16 1 #define SPK_ENABLE_INT16 1 #include "spark_astc.metalh" struct ColorData { half4 color [[color(0)]]; }; kernel void encode_tile(imageblock<ColorData> img, ushort2 tile_coord [[thread_position_in_threadgroup]], ushort2 gid [[thread_position_in_grid]], ushort2 grid_blocks [[threads_per_grid]], device uint4* output [[buffer(0)]]) { half3 block[16]; for (ushort y = 0; y < 4; y++) for (ushort x = 0; x < 4; x++) block[4 * y + x] = img.read(4 * tile_coord + ushort2(x, y)).color.rgb; uint idx = uint(gid.y) * grid_blocks.x + gid.x; output[idx] = spark_encode_astc_4x4_rgb(block, /*quality=*/1); } ### 2. Allocate a memoryless render target The render pass needs a color attachment, but its contents never leave tile memory, so use the `MTLStorageModeMemoryless` storage mode to avoid allocating memory for it: MTLTextureDescriptor* d = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm width:w height:h mipmapped:NO]; d.storageMode = MTLStorageModeMemoryless; d.usage = MTLTextureUsageRenderTarget; id<MTLTexture> tile_target = [device newTextureWithDescriptor:d]; ### 3. Set up the tile pipeline MTLTileRenderPipelineDescriptor* td = [MTLTileRenderPipelineDescriptor new]; td.tileFunction = [library newFunctionWithName:@"encode_tile"]; // same format as the attachment td.colorAttachments[0].pixelFormat = MTLPixelFormatRGBA8Unorm; // one thread per 4x4 block, not per pixel td.threadgroupSizeMatchesTileSize = NO; id<MTLRenderPipelineState> tile_pso = [device newRenderPipelineStateWithTileDescriptor:td options:0 reflection:nil error:&error]; ### 4. Render, then encode, in one pass Draw your content as usual, then dispatch the tile kernel in the same render encoder: MTLRenderPassDescriptor* rp = [MTLRenderPassDescriptor renderPassDescriptor]; rp.colorAttachments[0].texture = tile_target; rp.colorAttachments[0].loadAction = MTLLoadActionDontCare; rp.colorAttachments[0].storeAction = MTLStoreActionDontCare; // required for a memoryless target id<MTLRenderCommandEncoder> enc = [cb renderCommandEncoderWithDescriptor:rp]; // ... draws that fill the color attachment ... [enc setRenderPipelineState:tile_pso]; [enc setTileBuffer:output_buffer offset:0 atIndex:0]; [enc dispatchThreadsPerTile:MTLSizeMake(enc.tileWidth/4, enc.tileHeight/4, 1)]; [enc endEncoding]; You can force a specific tile size `rp.tileWidth` and `rp.tileHeight`, not specifying them let’s the driver choose it automatically, and you can query the chosen values with `enc.tileWidth` and `enc.tileHeight`. The tile dispatch runs after all earlier draws in the render pass have finished and it processes the final pixels. A full example showcasing this is included in the Spark 1.4 SDK. ## What are the results? To benchmark this I generated the render target with three different shaders: a math-heavy procedural texture, a minimal shader with just a simple gradient, and terrain-splatting shaders like those used in virtual texture pipelines. They all behaved similarly, but the gains were largest on the terrain example, which is also the most realistic scenario, so it’s the one I’m presenting here. The following are timings in milliseconds of rendering a 1024×1024 terrain texture and encoding it to ASTC 4×4 at medium quality (Q1). Device| OS| Uncompressed| Compute| Tile| Tile vs Compute ---|---|---|---|---|--- iPhone 8 – A11| iOS 16.7.16| 0.839| 1.184| 0.834| 1.42x iPhone 12 – A14| iOS 26.6.2| 0.470| 0.782| 0.567| 1.38x iPhone 15 Pro – A17| iOS 26.6.1| 0.311| 0.497| 0.301| 1.65x iPhone 16 – A18| iOS 18.7.3| 0.332| 0.477| 0.317| 1.50x M4 MacBook Pro| macOS 15.7.7| 0.073| 0.119| 0.098| 1.21x To get stable measurements I ran each test 256 times and averaged the results. I also changed the GPU performance state to medium in order to obtain more stable clocks, so these results are not representative of peak performance. On every device I measured, the tile path beats the separate compute pass by 1.2–1.7×. On three of the four iPhones (A11, A17, A18), producing the compressed texture in the Tile Shader is as fast or faster than writing the uncompressed one. The MacBook Pro benefits less, probably because it has more bandwidth and larger caches, so the uncompressed store is not the bottleneck. It’s unclear why the A14 lags behind. The following diagram illustrates where the time goes (box lengths are approximate, not to scale): Uncompressed 4 B / pixel Generate texture 4 B write Render + compute 11 B / pixel Generate texture 4 B write Encode texture 4 B read + 1 B write Copy to texture 1 B read + 1 B write Tile shader 3 B / pixel Generate + encode 1 B write Copy to texture 1 B read + 1 B write The compute path first writes the uncompressed RGBA8 texture (4 B/pixel), reads it back (4 B/pixel), writes the ASTC output to a buffer (1 B/pixel), and finally copies that buffer into the compressed texture (another 2 B/pixel for the read and write): 11 bytes of memory traffic per pixel. The tile path eliminates the uncompressed write and read entirely. It encodes directly from tile memory, leaving only the compressed output and its final copy: 3 bytes per pixel. That’s even less traffic than simply writing the 4-byte-per-pixel uncompressed render target, which explains how rendering and encoding together can be as fast as rendering alone. These numbers exclude texture sampling, which is identical across all three paths. I double-buffered the output buffers and render targets so the copy overlaps with the next pass. Writing directly to the compressed texture through heap aliasing, as described in this earlier post, would remove the copy and narrow the gap to uncompressed further, but it isn’t officially supported, so I didn’t use it here. ## Conclusions Tile shaders make runtime texture compression considerably more attractive on Apple GPUs. Instead of rendering an uncompressed texture to memory and reading it back for compression, _Spark_ can encode directly from tile memory. That means the usual benefits of compressed textures (lower memory use, lower bandwidth, and faster subsequent rendering) can be obtained without paying a significant runtime encoding cost. Tile shaders are not exclusive to Apple hardware. They are also available on Qualcomm GPUs under Vulkan through the VK_QCOM_tile_shading extension. I haven’t yet experimented with it, but I will as soon as I can get my hands on a device supporting it.
011 1
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 17/09/2026
OK, Spark 1.4 is finally out! The highlight is HDR: new BC6H and ASTC HDR codecs, offline quality at real-time speeds. Also: BC1 Ultra, a BC7 codec for packed material maps, ASTC 5×5, and broader device support. www.ludicon.com/castano/blog/2026/0…
ludicon.com
Spark 1.4
I’m excited to announce the release of _Spark_ 1.4. _Spark_ is a set of real-time GPU texture codecs that produce results competitive with offline encoders while running thousands of times faster, on every GPU from budget phones to high-end PCs and consoles. The highlight of this release is HDR. _Spark_ 1.4 adds two new HDR codecs, **BC6H** for desktop and consoles and **ASTC HDR** for mobile, and both deliver quality that matches or exceeds the best offline encoders at real-time speeds. Real-time HDR compression opens up a new range of applications: * **Light probes and reflection maps** that are re-rendered as the scene changes and compressed on the fly. * **Dynamic lightmaps** for time-of-day cycles and destructible environments. * **Dynamic skies or any other HDR render target** whose contents can be cached across frames. * **HDR images** decoded at runtime, from EXR, AVIF or JPEG/R, without the need for an offline pipeline. **What’s new in Spark 1.4:** * **New HDR codecs** : BC6H and ASTC HDR. With offline quality at real-time performance. * **BC1 Ultra** : a new quality level for BC1 that surpasses the perceptual quality of offline encoders at a fraction of their cost. * **BC7 NRM** : a new BC7 codec for packed material maps that store normals, roughness and metalness in a single texture. * **ASTC 5×5** : a new block size that fills the gap between our 4×4 and 6×6 codecs. * **Broader compatibility** : PowerVR Series D (Pixel 10), Vulkan 1.0 devices, improved GLES `mediump` support, and improved PlayStation 5 support. This release also includes a new Getting Started guide and Performance Guidelines, many new examples, improved regression testing, bug fixes, and many other small quality and performance improvements. It’s exciting to see _Spark_ gaining momentum and shipping in a wide range of games and applications. Check out our front page for the latest additions to our client list. ## HDR Codecs Until now, _Spark_ handled HDR content through our RGBM codecs, which pack an HDR range into an LDR format. RGBM works everywhere, but it requires a decode in the shader and its range is limited. The two new codecs target the native HDR formats instead: the compressed texture is sampled directly and decodes to 16-bit floats, so there’s no color transform and no shader changes on the sampling side. ### BC6H Our previous codecs deliberately target a subset of their format: the modes and partitions that give the best quality per cycle within a real-time budget. The new BC6H codec breaks from that strategy. It is our first codec that can target the _entire_ format: all 14 modes and all 32 partition shapes. Which subset of the format the codec targets is configured at compile time, and we provide four presets. At the lowest level, the codec saturates memory bandwidth just like our other real-time codecs. At the highest level, it produces results that challenge the best offline encoders while running orders of magnitude faster. This chart shows the SSIMULACRA2 score of each encoder against its throughput on a M4 MacBook Pro, over a set of 27 HDR light probes, lightmaps and photographs. Larger scores are better. _Spark_ at its highest quality level scores above every other encoder, and nearly as high as OodleTex, while running 400× faster. Compared to the existing real-time BC6H encoders, the picture is even clearer: What I find most remarkable is that these results are achieved with a single candidate encoding. There’s no error evaluation and no comparison between multiple candidate encodings: the codec chooses a mode and a partition up front and never looks back. This is what enables these codecs to run well on the GPU. Compared with Krzysztof Narkowicz’s codec, _Spark_ delivers comparable performance while avoiding some of its worst-case artifacts, as the following image comparisons show. Spark Q1 Spark Q2 Narkowicz Q1 Narkowicz Q2 88.262 Spark Q1 Spark Q2 Narkowicz Q1 Narkowicz Q2 88.262 ### ASTC HDR One of the surprises of this release is the new ASTC HDR codec. I did not plan to include it, but the initial results were so promising that I could not stop myself. This first version is intentionally simple: it uses all the HDR endpoint encoding modes, but a single partition and a single weight and endpoint bit distribution. With these constraints I did not expect great results, but it turns out that the algorithms I developed for BC6H also work exceptionally well with ASTC, and even this first version matches or exceeds the perceptual quality of the existing offline encoders. At its highest quality level, _Spark_ scores above Arm’s astcenc at its most exhaustive setting and above every UASTC HDR preset except the slowest, while encoding on the GPU thousands of times faster than these CPU encoders on the same machine. The medium quality level matches astcenc’s at the default setting. The existing ASTC HDR encoders minimize the mean squared error in the linearized logarithmic space of the format. As a result, they often spend bits on features that are not perceptually noticeable while under-allocating elsewhere, and that results in errors that are visible with the naked eye. _Spark_ is optimized for perceptual metrics instead: Spark Arm Basis 86.620 Spark Arm Basis 86.620 Spark Arm Basis 86.620 Spark Arm Basis 86.620 As far as I know this is the only real-time ASTC HDR encoder publicly available, so there’s nothing else to compare it against on mobile. Here are the three quality levels on a Pixel 8 (Mali-G715): A 1024×512 light probe compresses in 0.5 ms at the highest quality level, and 0.17 ms at the lowest. Like all of our codecs, I’ve taken special care to ensure the code is fully vectorized for 16-bit arithmetic, and that the resulting code also performs well on Adreno. Even though Adreno does not support packed math instructions, in practice the vectorized variants result in less register shuffling, increased occupancy, and a reduced instruction footprint, so the same shader works well on every mobile GPU. Some mobile devices support both ASTC HDR and BC6H. Our BC6H codec was developed with PC and consoles in mind, but at the lower quality levels it also runs great on mobile. The following chart shows both codecs on an Adreno 740: ## BC1 Ultra BC1 is the oldest block compression format we support and the one most encoders have had the most time to mature, so I did not expect much headroom. The new _Ultra_ quality level proved me wrong. Modern techniques combined with perceptual quality optimizations result in higher quality than the existing offline encoders, and it still runs orders of magnitude faster than them. Three things make the difference: the use of the 3-color mode for blocks with near-black texels, a novel endpoint quantization procedure that takes into account the residual quadric of the least squares solver, and a greedy endpoint refinement. At this quality level it is very difficult to see any difference. The results are nearly indistinguishable from the offline codecs. ## BC7 NRM Our existing BC7 codecs are optimized for color images, but BC7 textures are often used to store other material attributes. One of the most common arrangements is to pack two components of the normal in the RG channels and roughness and metalness in the BA channels. Our color codecs were doing a reasonable job on this kind of data, but it was clear we could do better by tailoring the coding choices to it. Our new codec is optimized for the output of typical shading models, with special attention to smooth normals on glossy surfaces, where a small error in the normal becomes artifacts in the specular highlight. This codec has mostly been tuned with client assets that I’m not able to share publicly, but at least I can share some numerical results. The chart below measures the quality of the reconstructed normals with SSIM, which correlates much better with the appearance of the shaded surface than the raw error. ## ASTC 5×5 Targeting large block sizes in GPU encoders is difficult due to the high register pressure. _Spark_ 1.2 introduced our first streaming codec with support for ASTC 6×6 blocks. This codec processes one texel at a time, so it does not need to keep all the block colors in registers at the same time. This release refines that strategy and extends it to the 5×5 block size, offering a compression ratio in between the 4×4 and 6×6 codecs and giving developers a finer-grained quality and size tradeoff. ASTC 4×4 ASTC 5×5 ASTC 6×6 ASTC 4×4 ASTC 4×4 ASTC 5×5 ASTC 6×6 ASTC 4×4 ASTC 4×4 ASTC 5×5 ASTC 6×6 ASTC 4×4 Larger blocks reduce bandwidth, but not necessarily the memory footprint. See our previous investigation for the details: The True Size of ASTC Textures. ## Broader Compatibility We continue expanding compatibility in both directions: to new devices as they come to market, such as the Pixel 10 and 11, and to legacy devices, such as Vulkan 1.0 hardware, that we had previously thought non-viable. ### PowerVR Series D I’m excited to see PowerVR making a comeback in Android phones with the Series D GPUs in the Pixel 10. I’m happy to report that _Spark_ runs great on this new hardware, and that some outstanding issues from previous devices appear to be fixed and no longer require cumbersome workarounds. Series D is also the first PowerVR generation with ASTC HDR support, so the new codec runs there too. ### Vulkan 1.0 This release adds support for many Vulkan 1.0 devices that I had previously written off as non-viable. This includes many early Arm devices, such as the Galaxy S8, as well as some Adrenos, such as the Adreno 618 in the Pixel 3a. In the past, bugs unrelated to the codecs themselves had prevented my examples, demos and benchmarks from running on these devices, and I had assumed Vulkan 1.0 was simply too unstable to be practical. On closer inspection, it turned out to be possible to work around these issues and prove that the codecs run fine on these devices. ### Improved GLES `mediump` support Improving `mediump` support on GLES mostly came down to tracking down and fixing bugs in the `spirv-opt` and `spirv-cross` toolchains, and those fixes have been contributed upstream (SPIRV-Tools #6690 , SPIRV-Cross #2633). ### PlayStation 5 The PlayStation 5 codecs have been through extensive testing and a round of optimizations, and the SDK now includes a PS5 example and integration guidelines with performance results for every codec. ## Final Words Earlier this year I released _Spark for Unity_, which brings the codecs to the Unity engine, and spark.js brings them to WebGL and WebGPU. Both will pick up the new codecs in a follow-up release, and the Android and iOS demos will be updated shortly. If you want the longer story behind this work, I recently appeared on the Wookash Podcast, where we talked about texture compression, _Spark_ , and much more. If you’d like to try the new codecs in your own engine, or for licensing inquiries and anything else, reach out at spark@ludicon.com.
012 2
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 14/09/2026
Some results of the upcoming ASTC HDR codec. Arm and Basis are at the highest quality leve, and on my M4 they run at 1.23 and 0.7 Mpix/sec respectively. Spark is a very simple codec, but it's optimized for perceptual quality. It scores as high as the […] [Original post on mastodon.gamedev.place]
002 2
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 12/09/2026
I thought all Mali devices supported ASTC HDR, but looks like early Vulkan drivers (version 19 and earlier) do not expose it. @thesolidpixel Am I missing something?
000 0
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 12/09/2026
I thought one of the nice things about ASTC HDR is the limited number of devices I need to validate, since it's not available everywhere. However, it turns out that it's only 6 out ~30, so it's not much of a relief. Lots of minor precision issues across many devices. At least I don't have to […]
mastodon.gamedev.place
Original post on mastodon.gamedev.place
100 1
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 11/09/2026
Performance on Pixel 9 is alright. Bandwidth limited at the fastest preset, slower otherwise.
000 0
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 11/09/2026
This is ridiculously good. Now I just need to make it fast.
101 1
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 05/09/2026
There are still things I'd like to improve, but I think this is going to be good enough for this release: ludicon.com/spark/report/1.4/bc6h.h… BC6 is the highlight of Spark 1.4, but I ended up adding a ton of new stuff. Several new codecs, many optimizations, new examples, new […]
mastodon.gamedev.place
Original post on mastodon.gamedev.place
000 0
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 04/09/2026
OodleTex's source code is now available online, so I peeked at the sources to see if they did something similar. They did not, but interestingly I found some comments that seemed to point in the same direction. I decided to take a stab at it and […] [Original post on mastodon.gamedev.place]
100 1
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 04/09/2026
I'm trying to get things ready for a new release of the Spark codecs. While testing the new BC6 codec on a wider range of images I run into some artifacts I had not seen before. Digging into it I came up with an understanding of the problem and some […] [Original post on mastodon.gamedev.place]
100 1
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 03/09/2026
Support for Firefox is going well: github.com/gfx-rs/wgpu/pull/10247 Once it's supported in wgpu it should be trivial to enable it in Firefox.
github.com
Add TEXTURE_COMPRESSION_UNALIGNED feature by castano · Pull Request #10247 · gfx-rs/wgpu
Implements the WebGPU texture-compression-unaligned feature (gpuweb/gpuweb#6312), which allows creating block-compressed textures whose size is not a multiple of the texel block dimensions, i.e. te...
010 1
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 02/09/2026
This is important for spark.js, because it enables you to load any arbitrary image and encode it without having to resize it to meet this constraint. In some cases the resize can introduce more artifacts than the encoding itself. There's a spark.js example that showcases this feature and shows […]
mastodon.gamedev.place
Original post on mastodon.gamedev.place
000 0
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 02/09/2026
This is exciting: github.com/gpuweb/gpuweb/issues/200… A WebGPU feature/extension that I proposed has finally been accepted! 🎉 It adds support for block compressed textures that are not aligned to the block size. Most APIs support this, except D3D11 and early […]
mastodon.gamedev.place
Original post on mastodon.gamedev.place
310 3
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 31/08/2026
Hobbit Book did not disappoint (the climb, not the book)
000 0
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 30/08/2026
And problem solved! Found a workaround that does not regress in quality or performance. Bug filed with the IHV.
000 0
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 29/08/2026
The joys of adding support for a new platform. Why are you doing this compiler?
Small pixel artifacts on an encoded texture.
100 1
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 27/08/2026
RE: mastodon.social/@verge/117168931003… I wonder if I should try to capitalize on this and how. Spark is one of the easiest ways to reduce your app memory footprint if you have any uncompressed textures lying around. However, it seems to me the average developer doesn't care […]
mastodon.gamedev.place
Original post on mastodon.gamedev.place
000 0
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 23/07/2026
I heard good things about Unwrella, but, at least on this mesh, it doesn't seem to work that well. None of the modes it provides produces pleasing results.
010 1
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 23/07/2026
Someone mentioned isocharts today (Microsoft's UVAtlas). I remember evaluating it on The Witness, back when it was closed source and not being happy with the results. I just gave it another try and it doesn't seem to work particularly well.
remeshed craftsman uv-mapped with Microsof'ts uvatlas (isocharts).
120 2
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 22/07/2026
Hi Siggraph!
Me in front of the siggraph sign
010 1
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 12/07/2026
My favorite swimming hole
041 4
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 11/07/2026
I'll be in LA next week! Not for SIGGRAPH, but for my daughter's water polo tournament. I'll have Wed/Thu (July 22–23) open to meet clients and partners while I'm there. Might also grab a SIGGRAPH day pass if I'm not too busy. Reach out if you want to connect.
000 0
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 08/07/2026
The relief mapping on the snow looked really fake.
Sun cups on the snow field below Lamarck col.
121 2
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 07/07/2026
In my happy place
010 1
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 29/06/2026
My friend Art is a mountain ultrarunner. He got injured training for a 120 mile race. His abs got detached from his pelvis while running downhill. Despite of that he can still hike and he leaves me in the dust.
000 0
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 22/06/2026
I spent the last 10 days camping, hiking and backpacking in Yellowstone and the Tetons. It’s such an amazing place! and it was so nice to disconnect; I should do this more often.
Yellowstone backcountry camp with a Bison pacing in the background.
100 1
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 15/06/2026
What a disgrace
000 0
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 11/06/2026
Lumen suggested adding weight pruning. That actually works very well, is trivial to implement and speeds things up by about 30%, making this the fastest CPU implementation available.
000 0
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 11/06/2026
I've published a repository with my ssimulacra2 implementation at: github.com/Ludicon/ic-metrics I use this to tune all my codecs and generate the distortion maps in ludicon.com/sparkjs/#Examples I shared this a while ago through a […] [Original post on mastodon.gamedev.place]
ssimulacra2 distortion map showing the combined error of AVIF and BC7 compression.
110 1
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 09/06/2026
The Spark for Unity demo is finally available on Google Play. ludicon.com/spark/#demo Approval took longer than I expected, but I also expected some kind of notification and it never materialized, so I only noticed now that the app was available on the store. #unity #gamedev
071 7
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 06/06/2026
Well, that was a surprise. I swam faster than the last three years, despite having a crappy night of sleep and not feeling particularly strong lately.
000 0
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 06/06/2026
One more year as the race setup coordinator for our annual open water swim / fundraiser. What a pleasure with such a great group of volunteers. Very windy tonight, hoping all our hard work doesn’t blow away during the night!
Lake Berryessa
010 1
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 04/06/2026
Source code is on GitHub: github.com/ludicon/spark-unity Free for projects under $100K in revenue, $1K/$10K commercial tiers, lifetime distribution rights. Same model as spark.js. Happy to answer questions.
github.com
GitHub - Ludicon/spark-unity: Demonstration of how to use the Spark codecs in Unity
Demonstration of how to use the Spark codecs in Unity - Ludicon/spark-unity
000 0
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 04/06/2026
The demo app is available for iOS, Android, macOS, and Windows. ludicon.com/spark/#demo
110 1
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 04/06/2026
Texture2D compressed = Spark.EncodeTexture(source, SparkFormat.RGB); Replaces calls to the unity Unity built-in encoder with a single call, but running on the GPU. It produces higher quality results and is orders of magnitude faster.
100 1
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 04/06/2026
I'm finally ready to announce Spark for Unity, a Unity package that brings the Spark texture codecs to the Unity engine and it's free for projects with lifetime revenue under $100K. www.ludicon.com/castano/blog/2026/0…
ludicon.com
Spark for Unity
Today I’m releasing _Spark for Unity_ , a Unity package that brings the _Spark_ texture codecs to the Unity engine. It’s available now on GitHub and it’s free for projects with lifetime revenue under $100K. Unity’s built-in runtime texture compression is CPU-bound, limited in format support, and often too slow for real-time use cases. _Spark for Unity_ brings GPU-native texture compression to Unity, making it practical to compress textures at runtime in real-time. Texture2D compressed = Spark.EncodeTexture(source, SparkFormat.RGB); That single line takes an existing texture and produces a standard `Texture2D` in a GPU-native format (BC7 or ASTC depending on the device), ready to assign to a material. It runs entirely on the GPU, at least an order of magnitude faster than Unity’s `Texture2D.Compress`, and produces higher quality results. The package works across all Unity render pipelines (URP, HDRP, and Built-in) without modification. It’s been tested on Metal (macOS, iOS), Vulkan (Android, Windows), OpenGL ES 3.1 (Android), and Direct3D (Windows), on Unity versions 6.3 to 6.6. If you want to see it running, the demo app is available for iOS, Android, macOS, and Windows. ## How _Spark_ compares to `Texture2D.Compress` Unity ships with a built-in runtime compressor, `Texture2D.Compress`. This is the natural choice for anyone compressing textures at runtime in Unity. However, the built-in compressor runs on the CPU, and uses a single thread. Texture compression is intrinsically parallel and therefore maps very well to the GPU. The GPU executes this work much more efficiently, and because it runs off the main thread, it doesn’t block frame execution. On GPUs that support it, _Spark_ can also run on the async compute queue, overlapping with rendering work and reducing the total frame cost further. Runtime-generated textures make the case even stronger. Source texture data is often generated in the GPU (mipmaps, procedural shaders, render targets, virtual texture tiles) and is always consumed by the GPU. Compressing this data on the CPU would require a read-back and upload which would dramatically reduce performance. Encoding on the GPU is the natural fit for the data flow. ### Supported Formats The compressed formats available through the built-in API are limited. Modern formats like ASTC and BC7 are not supported, and format selection is dictated by the target platform rather than the underlying device. | `Texture2D.Compress`| _Spark_ ---|---|--- R| BC4 / EAC_R| BC4 / EAC_R RG| BC5 / EAC_RG| BC5 / EAC_RG RGB| BC1 / ETC2| BC7 / ASTC / BC1 / ETC2 RGBA| BC3 / ETC2| BC7 / ASTC _Spark_ supports both high-quality (BC7, ASTC) and low-bitrate (BC1, ETC2) formats for RGB content, letting you choose based on memory and quality requirements. For RGBA, _Spark_ targets BC7 and ASTC exclusively. These formats are widely available, have the same memory footprint as BC3 and ETC2_RGBA, and produce higher quality results. Note that many iOS and Android devices now support BC formats. The built-in path never targets BC formats on those devices; it defaults to ETC2. _Spark_ , in contrast, selects the best format the device supports. ### Performance Encoding a 1024×1024 texture takes about 1 ms with _Spark_ on a budget Android device like the Galaxy A15, compared to over 10ms with the built-in encoder. On desktop and high-end mobile devices, performance is significantly higher. The chart below compares performance across desktop, high-end mobile, and budget mobile: The demo app includes a benchmark that runs the full comparison on your own device. ### Quality _Spark_ and the built-in encoder make different format choices, so a direct comparison isn’t as clean as the throughput chart. Here’s how each performs on the formats they share, measured as RMSE (lower is better) on an 8-bit scale. Note that _Spark_ ’s codecs are tuned for perceptual quality, which RMSE captures only partially. The visual examples later in the section are a more representative. **Desktop (BC) formats:** | Format| Texture2D.Compress| Spark ---|---|---|--- R| BC4| 3.96| 0.55 RG| BC5| 6.27| 1.89 RGB – lo| BC1| 4.91| 4.41 RGB – hi| BC7| N/A| 2.16 RGBA| BC3 / BC7| 3.60| 2.74 **Mobile (ETC / ASTC) formats:** | Format| Texture2D.Compress| Spark ---|---|---|--- R| EAC_R| 0.69| 0.78 RG| EAC_RG| 2.12| 2.49 RGB – lo| ETC2| 4.57| 4.58 RGB – hi| ASTC| N/A| 2.32 RGBA| ETC2 / ASTC| 4.29| 3.19 The mobile ETC2 codecs in Unity are competitive with _Spark’_ s, and slightly better on some single- and dual-channel formats. _Spark for Unity_ ships the lower-quality ETC variants by default to maximize throughput on mobile; higher-quality ETC encoders are available in the full _Spark_ codec suite, but the difference rarely makes a perceptual impact at runtime. The BC story is different. Unity’s built-in BC implementations produce visible artifacts on most content, while _Spark_ ’s output is much closer to the original: Unity built-in Spark Unity built-in Unity built-in Spark Unity built-in The tables also miss an important advantage. On many modern iOS and Android devices, _Spark_ can target BC formats directly, while Unity’s built-in compressor is restricted to ETC2. In practice this often means both higher quality and faster encoding on the same device. ## The Demo The demo focuses on two scenarios that exercise _Spark for Unity_ in different ways. **Procedural virtual texturing.** A simplified virtual texturing pipeline built around a single 16 MB compressed atlas and an indirection texture. The virtual texture itself is 1,048,576 × 1,048,576, far larger than would fit in memory without virtualization. As the camera moves, the system identifies which tiles are visible, evaluates the Mandelbrot fractal on the GPU to render them, compresses them with _Spark_ , and copies the results into free slots in the atlas. The indirection texture is updated to point each visible region at its newly-compressed tile. This isn’t a production virtual texturing engine, but the minimum pipeline that exercises the runtime-compression part of the problem. It demonstrates the use case: _Spark_ makes it practical to compress tiles on the same frame they’re generated, without leaving the GPU, and without significant impact to quality or performance. **Runtime texture transcoding.** Textures distributed as PNG or JPEG, placed in `StreamingAssets` so they bypass Unity’s build-time processing and ship as-is. At load time, the demo decodes them and uses _Spark_ to compress them to the appropriate GPU format. The same approach works for glTF assets: the demo loads them with glTFast and provides a custom image loader that compresses textures at load time. Full source code is in the GitHub repository. ## How it Works The basic technique behind GPU texture compression in Unity was demonstrated by Aras Pranckevičius in UnityGPUTexCompression: run the encoder in a compute shader, write the compressed blocks to a UINT `RenderTexture`, copy the `RenderTexture` into a `Texture2D` with the appropriate compressed format. _Spark for Unity_ follows the same steps, with support for more platforms and texture formats, and wraps everything in a simple C# API. The reason I hadn’t released a Unity integration before is that this approach requires the shaders to be distributed as source code. That’s fine for an open-source proof of concept, but not workable for _Spark_. Source code is only available to licensees. The solution is to ship the shaders as pre-compiled HLSL header libraries. The pipeline is: 1. Compile the codec shaders to a SPIR-V library with DXC. 2. Process the SPIR-V with `spirv-opt` to optimize the code. 3. Use SPIRV-Cross to translate the SPIR-V library to a single-file HLSL header library. You can drop the resulting `.hlsl` files into any Unity project and it just works. Unity translates the HLSL libraries to the appropriate native shader format for each target platform at build time, which is why the same library works across Vulkan, Metal, and Direct3D. This pipeline required changes to DXC’s SPIR-V compiler and to SPIRV-Cross. Some of those changes are already upstream in the KhronosGroup/SPIRV-Cross repository; others are still under review but stable enough for production use. ## Licensing _Spark for Unity_ is available under the same licensing terms as spark.js: * **Free** for projects with lifetime revenue under $100,000. * **Indie** ($1,000 one-time fee) for commercial projects with development budget under $1 million. * **Pro** ($10,000 one-time fee) for projects beyond that. All tiers include lifetime distribution rights. The C# code is released under the MIT license; the _Spark_ shaders are covered by the _Spark_ EULA. This release relaxes the free tier from “non-commercial only” to projects under $100K in lifetime revenue. The change applies to spark.js as well. The two products now share the same terms. Studios with custom pipeline requirements, console targets, source access, or production support needs can license the full _Spark_ codec suite under an enterprise agreement. See the _Spark_ licensing page for details. ## Try it _Spark for Unity_ is on GitHub at github.com/ludicon/spark-unity. To install via Unity Package Manager, add this Git URL to your project’s manifest: https://github.com/ludicon/spark-unity.git#v0.1.0 The basic API is a single call: Texture2D compressed = Spark.EncodeTexture(source, SparkFormat.RGB); For most use cases, that’s everything you need. The lower-level API exposes additional controls for advanced scenarios: batching multiple encodes, scheduling work on the async compute queue, updating individual mip levels or texture regions, and preallocating output textures to avoid per-frame allocations. See the README for more details. If you want to see Spark for Unity running before integrating it, the demo app is available for iOS, Android, macOS, and Windows. It includes the scenes shown above. ## What’s next? The same pre-compiled HLSL libraries that power _Spark for Unity_ are portable to other engines. Integrations for Unreal, Evergine, and others are within reach, and I’m interested in talking to anyone working in those ecosystems who’d want to collaborate. Working through the Unity integration also surfaced a number of cases where _Spark_ ’s performance is gated by Unity’s compute shader infrastructure rather than by the codecs themselves. Some of these are addressable with source code access; others require changes in Unity. I’m hoping to work with the Unity team on the ones that need their attention, and I’ll write up the technical details in a future post. _Spark for Unity_ is available today on GitHub. If you’re building apps with user-generated content, procedural textures, virtual texturing systems, or simply need better runtime texture compression, give it a try and let me know how it works for your project. If you’d like to talk in person, I’ll be at SIGGRAPH this summer.
110 1
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 04/06/2026
My kid is really into the silkson soundtrack, so we couldn’t miss this!
000 0
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 03/06/2026
So smooth! www.youtube.com/watch?v=mGip64OyygU
000 0
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 01/06/2026
NVIDIA RTX *Spark*? seriously? Sigh.
000 0
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 30/05/2026
RE: mastodon.gamedev.place/@rygorous/11… What a treat to wake up a Saturday morning with a new blog post by @rygorous
mastodon.gamedev.place
000 0
Ignacio Castaño🍉 @castano.mastodon.gamedev.place.ap.brid.gy · 28/05/2026
On other news, my Google Play account was restored, so I'll be able to publish the Android build shortly.
000 0