Error 403 Forbidden
Forbidden
Error 54113
Details: cache-lga21959-LGA 1787100909 1777502538
Varnish cache server
10 interesting stories served every morning and every evening.
Error 403 Forbidden
Forbidden
Error 54113
Details: cache-lga21959-LGA 1787100909 1777502538
Varnish cache server
It’s not technically a tax. Taxes produce valuable public benefits, like medical research and parks. This is simply legal theft.
Amazon makes nearly a billion dollars in profit from search ads. Every week. Each week, they sell merchants and publishers enough search-distorting ads to capture a billion dollars in revenue. Amazon makes enough in search ad revenue to give every single one of their employees a $35,000 cash bonus and still have change left over.
My publisher is terrific, and they’re working hard to introduce people to my new book. Last week, they began buying search ads on Amazon.
At first glance, this is compelling. Someone who isn’t sure what they’re looking for, who is looking for a book or a kitchen appliance, might find one if the right ad showed up at the right time.
But of course, that’s not what yields, or what most of the ads you see on Amazon do.
If you’re searching for an air fryer, Amazon already knows quite a bit. They know the best-reviewed, least-returned, best-priced model. The only purpose of the ads is to get you to pick an air fryer that isn’t that one (or for the best air fryer, to keep you on track to buy the one you wanted in the first place). The ads make the search worse. [Cory wrote about this three years ago, and the scale has already doubled.]
When there are plenty of ads, the maker of the best air fryer now has to bid on ads as well, if only to protect the sales they were entitled to in the first place. Businesses continue to buy the ads—not because they’re dumb, but because the system has created a situation with few options. Folklore implies that buying the ads somehow shifts how search responds in the long run, even after the ads stop running, but there’s little data to confirm this.
Traditional ads increase demand. We see something that’s clearly an ad, it might spark desire, and sales go up. But zero-sum search ads aren’t like that–the total sales in the category stay the same, and merchants are merely competing for a share of a static pie. This study argues that an ecommerce site with search ads actually sells fewer items than the same site without ads.
The highest-yielding ad my publisher has tested so far is the search “Seth Godin The Knot“. It costs about a dollar per click. My publisher is paying Amazon a dollar to show you an ad for the book you went to buy in the first place.
Who ends up paying the more than $50 billion a year spent on these ads? It’s not the sellers. Sellers can’t make heartfelt donations for long. It’s you. By making the marketing of products significantly less efficient, Amazon’s theft makes products more expensive or sucks the energy out of the development of new products.
It leads to two perverse side effects. First, producers realize that if brand reputation matters less than a budget for clicks, they will shift to shoddy and cheap versions of their products so they have a bigger budget for clicks. And second, Amazon (and Google before it) have an incentive to make their organic search results worse–giving producers more incentive to buy more ads.
For decades, Amazon created value for consumers by lowering the price of just about everything. And they opened the doors to merchants who didn’t have sufficient distribution. They claimed to be customer-centric, and they were.
I don’t think they can claim this any longer. The ad system they built isn’t illegal, but it’s pretty clear who it’s for.
Amazon is stealing from the customers they said they were here to serve.
AI and ml
$10 million buys over 100 million emails, 30 million recorded phone calls, reams of stuff from Teams, Oracle, and SAP
Google has acquired some data of failed US airline Spirit.
Spirit hit financial turbulence when COVID-19 blew in during 2020 and started making losses. Its balance sheet never climbed back to a safe altitude and in May 2026 the airline grounded itself permanently.
The low-cost carrier entered liquidation to wind itself up and is now auctioning assets to raise cash and settle at least some of its debts.
REG AD
A court document [PDF] filed last week reveals that one of the assets up for sale is a huge trove of deidentified data, which Google bid for and won for just $10 million - pending judicial approval.
REG AD
For that sum, Google bought itself 100 million emails and 500 million items from Microsoft Teams, 17 million OneDrive files and 20.5 million items from SharePoint. The search giant also now owns over 30 million recorded customer service calls, and more than 15 million customer service chat records.
600,000 ServiceNow tickets are another element of the collection, along with 13.7 million active emails addresses from Oracle’s Responsys marketing application, and details of 11 million sales of in-flight Wi-Fi services.
There’s also operational data in the trove, describing over 763,000 flights, five million crew pairings, more than 1.2 million fuel slips, and records describing purchases of 787,452 parts.
Google has reportedly said it bought the data to improve its AI services. The underbidder was Mercor, a company that provides data to train AI models. So clearly Spirit’s data is of value to AI companies.
Indeed, The Register recently reported how AI experts increasingly believe large language models are blunt instruments, and that smaller models trained on specific fields of knowledge are more useful in some applications.
Google might have itself the basis for an aviation ops model, or just with all sorts of quotidian financial records that could be useful for another AI.
If you’ve flown Spirit and worry that Google will soon know about your flight history or a testy conversation you had with the airline’s call center, you’re being told not to worry. The court filing says the data was deidentified before being put on sale and Google has promised to scrub any PII it finds in the trove.
The Register awaits evidence of the inevitable SNAFUs that mean some personal info appears as the result of a future prompt, or search.
REG AD
Fasten your seat belts! ®
Earlier this year, I blogged about work I did to improve VRAM management for games. Now, after many months of floating around in mailing lists, the kernel patches are finally merged upstream and queued for Linux 7.3! Hooray!
To celebrate, let’s look a bit deeper at one sentence I wrote in my previous post:
[Games] should perform much more stable - as long as the game itself doesn’t use more VRAM than you actually have.
[Games] should perform much more stable - as long as the game itself doesn’t use more VRAM than you actually have.
So, one may ask: What if they do, in fact, use more VRAM than you actually have?
Typical expectations for this seem to be that once this happens you’re pretty much screwed. Games will start crashing left and right, performance plummets to unplayable levels, a good gaming experience becomes impossible.
But is that really just an unavoidable fact of life? What really makes running out of VRAM suck so hard? And, most importantly: How can we make it suck as little as possible?
Setting expectations
In theory, running out of VRAM should exclusively be a performance issue, not a stability one. Support for overcommitting VRAM has existed for as long as GPU drivers have: If the driver overcommits VRAM, you are generally allowed to request as much VRAM as you’d like, and you’ll get as much as the kernel driver decides it can fit into the physical memory that exists on GPU.
On the performance side, the big-picture reason for bad performance when you run out of VRAM is fairly simple. As soon as the game requests more VRAM than is physically present, some of the game’s memory will have to be moved/evicted to CPU RAM instead. For the GPU, accessing CPU RAM is much slower than VRAM: Not only is CPU RAM slower than a dedicated GPU’s VRAM in general, all memory accesses also have to go over the PCI bus. The PCI bus adds latency and is typically also the limiting factor in bandwidth when fetching from CPU memory.
Due to PCI speed limitations, there are some truly unavoidable performance constraints when overcommitting VRAM. Assuming the GPU is hooked up via a PCIe 4.0x16 connection, you get a little less than 32GiB/s of bandwidth. Each millisecond, that PCIe bus can transfer ~32.2MiB of data. For a minimum framerate of 30 frames per second (33.3ms per frame), the absolute maximum amount of data the GPU is able to access is ~1,075.5MiB, a tiny bit over 1GiB of data. In other words, if so much memory gets evicted that the GPU needs to fetch more than 1GiB from evicted memory in one single frame, it is simply impossible to still hit 30 FPS.
Not all memory is equal
At the same time, just reading a little bit of CPU memory on the GPU is not immediately a death sentence for performance. In fact, GPU drivers sometimes decide to let things like command buffer data and related allocations live in CPU RAM even when there’s plenty of VRAM available! Whenever the GPU executes these commands, it has to access CPU memory, and yet in these cases everything runs completely fine. So what makes these accesses different - why are they fine and yet running out of VRAM seems catastrophic?1
One thing that influences the calculus significantly is caching. Since the access latency in case of a cache hit is the same regardless of whether the cached memory lives on CPU or GPU, the high initial cost of fetching over the PCI bus can be amortized by cache hits (to some extent). We can estimate latency differences between fetching CPU RAM and VRAM by writing microbenchmarks that measure access latency for different buffer sizes (using an adversarial access pattern to minimize cache hitrates as far as possible). The result you get may look something like this (captured on RDNA3):
As expected, if the buffer fits into L2 (or any higher-level cache), access latencies are exactly the same for memory backed by CPU RAM and memory backed by VRAM, because the data gets fetched directly from cache in either case. At a size of 6MB (the L2 cache size on RDNA3), CPU memory latencies go up to about 2400 cycles per access, while device memory latencies stay within the same rough ballpark. Note that VRAM accesses also go through the Infinity Cache, but CPU memory accesses do not (they hit PCIe directly on an L2 miss). I suspect this is because the Infinity Cache sits directly on top of VRAM, so any access that doesn’t hit VRAM also doesn’t reach the Infinity Cache.
Obviously, memory doesn’t start off with being cached anywhere, so the first access will still have considerably higher latency. Also, losing the Infinity Cache definitely hurts as well: PCIe fetches seem to have somewhere around 7.3x as much latency than an Infinity Cache hit, and around 4.6x as much latency as a fetch from VRAM. This increased latency needs really high cache hitrates to fully amortize the cost of going over PCIe. That means there is only a small set of use cases where using CPU memory has such minuscule slowdowns that you’d actively decide to use it in favor of VRAM when you have the choice. When you’re evicting memory from VRAM, there will almost unavoidably be at least some degree of slower performance.
Still, even though slowdown is unavoidable, there is going to be memory where eviction matters more and memory where eviction has a lesser effect on overall perf. Memory that is accessed in very cache-friendly ways is not affected by the slowdown of CPU RAM as much. If the access patterns aren’t cache-friendly but the memory isn’t accessed very often, things may also still be fine since the GPU only rarely needs to actually fetch data from CPU RAM. There might be many memory allocations where the GPU will only access a small part of the total allocation size, and never even read the rest. If these allocations were to be evicted, you might evict multiple GiBs of data, but still remain well below the 1GiB hard limit of data that is actually accessed per frame.
All of these variables make it surprisingly hard to predict how performance actually pans out in practice when memory is being evicted. But in short: Depending on how much the evicted memory gets accessed and how well these accesses cache, you might just be able to run out of VRAM without (completely) ruining performance!
Confronting reality
We’ve theorycrafted ourselves all the way towards having performant VRAM overcommitment now. Great! Let’s just boot up SteamOS, start some game and crank up the setti- radv/amdgpu: Not enough memory for command submission.
oh.
As it turns out, running out of VRAM in practice does carry plenty of stability issues with it.
This error isn’t quite like a regular “couldn’t allocate, out of memory” error, though. Note that the message specifically complains about command submission: RADV prints this message when the kernel returns -ENOMEM when trying to submit commands2, but merely submitting commands does not allocate any new resources! All the command buffers were allocated in advance, and clearly their allocation succeeded. Even though all memory was successfully allocated, using it in a GPU submission suddenly results in “out of memory” errors being thrown.
It’s time for another kernel adventure! Surely getting the kernel to accept the submission can’t be that hard - after all, the kernel already accepted all the allocations3!
The horrors of kernel locking
One thing the amdgpu driver has to do on every submission, before it can direct the GPU to start executing commands, is to make sure that all memory that may potentially be referenced by the GPU commands is accessible. With more modern bindless graphics APIs, you have to assume all allocated memory may at some point get referenced. Therefore, amdgpu will try to make sure all allocated memory is also accessible.
Each memory allocation carries information about which type of memory (for our purposes here, system RAM or GPU VRAM) it can be properly accessed from. Most allocations can be accessed from either CPU RAM or VRAM, and amdgpu will be happy with the memory allocation being in either of these memory types. Some allocations, however, have to be placed in VRAM and VRAM only. If these memory allocations have been evicted to system RAM because some other application allocated VRAM in the meantime, amdgpu will have to move them back into VRAM. Because there is no free VRAM available at all, moving the allocation back requires evicting something else. For some reason, that failed and the kernel reported an out-of-memory condition.
In order to explain why evicting something randomly fails, we’ll have to take a small detour to look at how the kernel handles (CPU-side) locking for GPU allocations. In order to evict a memory allocation, you have to acquire a lock associated with that allocation. However, during a submission, you also have to lock every allocation that’s referenced in a submission, to prevent some other application from moving the allocation somewhere else while you’re busy preparing GPU work. But if another GPU submission is doing the same thing concurrently, you can end up in a situation like this:
If one submit wants to evict an allocation that another submit has already locked, but that other submit also needs to lock an allocation from the first one to make progress, we have a textbook ABBA deadlock condition.
But fear not, the kernel knows how to detect and resolve deadlocks! The details about how deadlock detection works are explained in this kernel documentation page, but in very broad strokes, the kernel associates locking operations with a “transaction” (which basically just keeps track of which locks were acquired). If two transactions would deadlock, one of the transactions is marked as “wounded”, and the next time it tries to acquire a lock, the -EDEADLCK error is returned. This error requests the transaction to be aborted: All locks acquired during the transaction should be released, and the transaction is restarted from scratch. In the context of command submission, this just means the driver will restart the process of going over all memory allocations and making sure they’re accessible.
So where’s the catch? There isn’t one. This approach is rock solid and works really well.
At least as long as it’s actually implemented everywhere.
In the graphics subsystem, the gritty internals of the wound-abort-retry loop are abstracted using a small helper library called drm_exec. Instead of having to manually track which allocations are locked, and release the locks once you run into -EDEADLCK, you simply use the drm_exec_lock_obj helper. If you study the locking code in TTM, the shared Linux GPU memory management layer, you will notice a profound lack of usage of drm_exec.
Instead, there even is a comment noting that -EDEADLCK will cause eviction to fail. There we go, we found our issue! As soon as this deadlock condition is encountered because of intense memory pressure during command submission, the kernel bails out and rejects the submission instead of retrying.
There already are some patchsets to hook up the drm_exec helper in TTM, sent all the way back in 2024, but those never made it in for a few reasons, among which were some remaining bugs that hadn’t been figured out. My work had been cut out for me here: Rebase the patchset on top of my kernel version and figure out what those remaining bugs are.
Rebasing the patchset wasn’t too much of a hassle, and figuring out the bugs only took one single week of intense suffering with games randomly hanging 3 minutes into heavy VRAM contention. Not the worst!
I tried resending the patchset with fixes for all bugs I found in the hopes it would get in this time, but there’s going to be more work needing to be done with it before it can be merged.
Now that running out of VRAM at least won’t crash your apps at random, we can at least properly crank up the settings and look at perf. The initial result gave me an absolutely glorious performance graph like this:
Hold On Where Did All The Perf Go
Figuring out why performance is so garbage requires figuring out what the system is actually doing that’s this slow. For broad “what’s the kernel driver doing??” questions like that, I like using gpuvis. gpuvis uses kernel tracepoints to build a timeline of things that happened (including “GPU work submission started/stopped”, from which the time taken for each submission can be inferred).
Booting up gpuvis with a trace taken while the system is running out of VRAM, the timeline shows a situation like this:
Turns out, most of that time isn’t actually spent on handling the submission (that’s the gfx_0.0.0 activity), but instead moving around memory in preparation for that submission (sdma0 activity)!
The reason why there are so many buffer moves all the time becomes more obvious if you use gpuvis’s event list, together with a filter to show only captured move events for a particular buffer object (I chose one at random here, most buffer objects have a similar pattern):
The list shows quite clearly that contending processes (in this case, gamescope and the game itself) will constantly take turns evicting and moving back the same piece of memory, over and over. That’s really bad! And it’s very reminiscent of something I wrote in my first blogpost:
Generally, two competing applications can be expected to roughly take turns executing GPU work - first one application submits work, then the other, then the first again, and so on. With that approach, memory would keep being moved back and forth after every single submission. One application gets kicked out and immediately moved back in, kicking the other out (which moves memory back in the next step). All this moving ended up with worse performance than if the memory had never been moved in the first place.
Generally, two competing applications can be expected to roughly take turns executing GPU work - first one application submits work, then the other, then the first again, and so on. With that approach, memory would keep being moved back and forth after every single submission. One application gets kicked out and immediately moved back in, kicking the other out (which moves memory back in the next step). All this moving ended up with worse performance than if the memory had never been moved in the first place.
This described an old issue where overly aggressive VRAM allocation would lead to ping-pong-like moves happening constantly. But that issue had since been fixed by simply not trying to claim VRAM when there isn’t any free VRAM left, and the kernel only started being somewhat aggressive when I implemented VRAM protection with dmem cgroups. Obviously, this must have reintroduced the ping-ponging somehow.
Conceptually, the design of the dmem cgroup VRAM protection should never result in ping-pong moves, because the kernel is only supposed to evict memory that does not have any cgroup VRAM protection associated with it. Without any VRAM protection, you should typically not be allowed to evict protected VRAM.
The single exception to this rule is memory that absolutely has to live in VRAM for things to work properly. These kinds of memory allocations are always allowed to be moved to VRAM to ensure system stability. Typically, almost nothing coming from an application is really required to live in VRAM for correct operation, but there is one buffer object coming from an application that does: The buffer containing image data to be scanned out to the display4.
Display hardware is funky
Not only does the display hardware like scanned-out images to be in VRAM, it also completely skips past the GPU’s virtual memory architecture and works with physical addresses exclusively. In consequence, scanned-out images also have to be contiguous in physical memory.
With virtual memory and the power of page tables, typical application buffers are only contiguous in virtual memory, and may be scattered around all over physical memory5. The first page of a buffer at virtual address 0x5000 may be mapped in the page tables to point to physical address 0x1234000, but the second page at virtual address 0x6000 might point to physical address 0x4321000, somewhere completely different!
Here is a diagram visualizing the mapping of virtual allocations to physical ones in case where there is a lot of fragmentation (which typically is the case when you’re very low on VRAM):
The arrows show page table mappings to physical memory segments for the different segments of the first allocation. They’re left out for all other allocations for readability.
If you’re allocating display scanout data, this fragmentation is not an option as the physical memory has to be contiguous. This has very, very unfortunate interactions with eviction of other data specifically. Let’s assume the scanout data has already been evicted, but now it’s time for that data to be scanned out, so it has to be moved back into VRAM.
Simply evicting one buffer won’t be sufficient, even if that buffer is the same size as the display scanout data, because evicting it does not result in enough contiguous physical space to place the scanout data in! To make matters worse, the eviction algorithm does not take into account physical memory constraints at all. It is a very simplistic loop along the lines of
while (true) { evict(getLeastRecentlyUsedBuffer()) if (tryAllocate(newBuffer) == SUCCESS) break; }
Using this algorithm (assuming the allocations are arranged in LRU order), even if you evict the first 3 allocations (green, blue, and red), there won’t be a large enough space to hold the scanout buffer! Even the largest possible free space is ever so slightly too small, as is visible in this updated diagram:
To find a large enough physically contiguous memory region in our example, every single allocation in VRAM would end up being evicted! In real-world scenarios, I observed up to 4GiB of VRAM being nuked just to make space for scanout images (which are ~32MiB of pixel data per image for a R11G11B10 pixel format). That’s going to hurt real hard! Simply the act of moving all that data out from VRAM would already cost at least ~130ms, according to the PCIe transfer rate estimated earlier.
Throwing heuristics at the problem
While scanout is definitely the most egregious failure case here, this issue is more general: There are always going to be certain memory allocations that will be moved to VRAM over and over, potentially kicking out some memory that an application might prefer to stay in VRAM. Resisting this and trying to move the evicted memory back in will most likely backfire.
Even though dmem cgroup protection is not a complete solution to this problem, it does reduce the problem scope by a lot. With cgroup protection, you can be sure that any random app won’t try to kick out important game resources willy-nilly. Any memory that does get moved back into VRAM by force probably has a good reason to be in VRAM. Therefore, even with dmem cgroup protection, we should be careful and not try to reclaim evicted memory back by force.
With some iterative testing, I think I’ve arrived at a set of heuristics that work reasonably well for most cases a game would encounter in the wild (not being too aggressive when stuff gets evicted by important system allocations is one thing, but it also needs to be reasonably quick at reclaiming evicted memory if e.g. the game is paused and the Steam menu runs instead, evicting lots of game memory, and then the game is resumed).
The heuristics work something like this:
When the kernel detects an application’s memory is being evicted, it enters a “hard throttle” phase for a few milliseconds. During this phase, it does not try moving any memory for that app back into VRAM whatsoever (as long as all memory can be properly accessed, of course).
After this period, it switches a “soft throttle” phase, during which it may reclaim free space by moving things back into VRAM, but does not try evicting any memory that other apps have allocated. This period may last up to a few seconds, to make extra sure everything reached a stable state.
If the “soft throttle” phase has completed without any further memory being evicted again, the system is assumed to have reached a fairly stable state and restrictions on evicting other applications’ memory are removed.
IME, this achieves an acceptable balance between not shooting oneself in the foot with overaggressive eviction of other apps, while still recovering reasonably fast when lots of your memory was suddenly evicted, for example because the game was paused and the user browsed around on Steam instead of playing.
Getting somewhere
With those heuristics in place, let’s finally try cranking up the settings for real this time.
I ended up going with Indiana Jones: The Great Circle, since it conveniently exposes a setting for streaming pool sizes that you can mess with to modify VRAM consumption pretty much directly.
Lo and behold, even if the settings are turned up to a somewhat ridiculous point, where the game requests 9GiB of 8GiB VRAM (aka. a whole 1GiB of overcommitted game resources living in CPU memory), performance isn’t cratering into oblivion anymore! A 19.6ms per frame average is what I’d still call perfectly playable.
I can also bump the settings to even more ridiculous levels and double the amount of overcommitted memory, with the game requesting 10GiB of VRAM on this 8GiB system (and thus 2GiB of resources being overcommitted). Frametime variance goes up quite a lot at this point, with spikes reaching above 33.3ms happening frequently. The overall average is around 29.8ms which isn’t the worst, but especially paired with the variance, this would start being noticeable in gameplay.
While this is already a huge step forward, we aren’t quite there yet. The experience under VRAM overcommit can sometimes still be a bit hit-or-miss, and frametimes may noticeably vary depending on which objects in the game you’re looking at.
Remember that for actually good eviction performance, it matters a lot how the evicted memory is used by the GPU. Right now, this isn’t taken into account at all! If we were able to base our eviction decisions more on how well the application’s accesses work with CPU memory, a lot of this variance might simply disappear.
Handing over the controls
The complicated thing about the application’s memory access patterns is that they are only really known to the application. Therefore, the driver isn’t really able to take them into account as-is. Ideally there would be some API where the application can supply hints to the driver about how well a particular memory allocation is suited to being evicted.
Something exactly like vkSetDeviceMemoryPriorityEXT! The VK_EXT_pageable_device_local_memory extension provides precisely what we need here, by allowing applications to communicate any priority they want for any piece of device memory they want. As long as applications provide reasonable hints through this extension, implementing prioritization in the kernel and then utilizing app-provided priorities has the potential to stabilize things by a lot!
Hooking up priorities in the kernel turns out to be a lot less of an issue than you might expect. The kernel already maintains a Least-Recently-Used list of memory allocations that, on eviction, are traversed in order. For each entry on that LRU list, eviction is attempted until there is enough free space for whatever the eviction was for.
This LRU list provides a good heuristic for which application’s memory should be evicted first. Applications that haven’t submitted anything in a long while are unlikely to need the memory soon, and since their memory is Not Recently Used, it will appear early in the LRU list and be evicted first.
When an application uses a set of buffers, that set of buffers is moved to the very end of the LRU list in one bulk. However, the order of allocations within that bulk is not explicitly controlled at all. That means once the kernel closes in on some application to evict its memory, which specific pieces of memory get evicted is more or less undefined6. A simplified visualization could look something like this:
If the kernel walks the LRU list like this, it would evict the buffer with a priority value of 2 first, even though there are much lower-priority buffers elsewhere in the LRU list. If only the first buffer of priority 2 gets evicted, things might be okay, but if the highly important buffer with priority 4 ends up evicted as well, there are likely going to be problems.
Given that we already know specific priorities for the individual allocations, this LRU list is a very simple place to integrate them. It’s as simple as ordering the list entries within a single application by their priority7:
Now, when the kernel goes over the LRU list to find something to evict, the very first thing it will find and try to evict are the lowest-priority buffers. The highest-priority buffers are last in the list, and thus only get evicted when evicting all the lower-priority buffers was not enough.
Memory priority adoption in apps
Unfortunately, not all applications actually set priorities via VK_EXT_pageable_device_local_memory. As for native Vulkan applications, I haven’t observed any idTech game using the extension directly, at least :/
The D3D side looks a lot better, because vkd3d-proton already uses VK_EXT_pageable_device_local_memory when available, and translates both the ID3D12Device::MakeResident/ID3D12Device::Evict API calls as well as priorities set via ID3D12Device1::SetResidencyPriority to priority values set using the Vulkan vkSetDeviceMemoryPriority command. Lots of D3D12 games utilize at least one of these APIs, so the hints these games provide will now be utilized.
I don’t have super solid numbers for how much memory exactly is overcommitted by most D3D12 apps, as they don’t typically expose the total amount of VRAM they request in an easy-to-access way like idTech’s performance overlay does. However, properly honoring memory priorities generally seems to have a good chance to improve the experience. Performance generally appears more stable over time (because you’re not relying on luck with which buffers the kernel evicts as much). In some spots I had a good comparison point at, I suspect it increased performance compared to the kernel evicting random things by up to 30% in the very best case - but again, take this number with a mountain of salt as it depends almost entirely on luck with regards to eviction.
Conclusion
When all is said and done, how well does running out of VRAM hold up?
I’d say it’s quite alright! In many cases, you may be surprised how much performance you can retain even when evicting a gigabyte or more of memory! Then again, that’s of course a rather optimistic case, and the wrong thing ending up in CPU RAM can very quickly cause very significant slowdowns. Eviction is tricky to get just right, and to an extent, performance will always be dragged down. If a game is struggling to hit 30fps even with everything in VRAM, needing to evict something on top of all that could sometimes just unavoidably result in that 30fps target being missed.
Regardless, what I hope this blogpost can demonstrate is that even if you end up with some memory evicted to system RAM, the slowdown can be manageable. There’s measures that drivers (particularly, the kernel driver) can take to make overcommit work as fast as possible, and even applications can do their part in coordinating with the driver stack to mitigate the effects of their memory being evicted. With everything in place, VRAM overcommit isn’t really as big of a deal as one may think it is at first sight.
All the work I described here has already been released in SteamOS for some time now (it’s both in Stable and Preview. As long as your system is up-to-date, it’s good to go!).
A note on upstreaming
Of course, I’m already working on upstreaming all this work so it’s available to everyone! However, there’s a lot of moving parts and a lot of deep refactors of some pretty core concepts at play here, so it will likely need time to cook before everything is merged upstream.
At the same time, I don’t want to put up a blogpost talking about lots of cool code just to finish it with “actually you can’t see for yourself, go wait until it’s all upstream lol”, either.
As a middle ground, I have rebased the kernel work onto a recent upstream version of the kernel and published a git branch here. While it should theoretically yield similar effects, it did not go through as rigorous testing the SteamOS kernel did. There will likely be bugs and instabilities that weren’t there in the SteamOS version. Use at your own risk, basically. I don’t expect to be maintaining this branch in any significant capacity, as I’d rather focus on getting the patches into upstream properly.
In order to pass through application priority hints to the kernel, you will also need a custom Mesa branch I pushed here. Similar considerations as the kernel branch apply here, as well.
Questions of my own
While I would claim to have a fairly good overview of the driver side of memory management at this point, I am not very familiar with how applications decide on supplying memory management heuristics internally, at all. I would suspect optimizing cases where you’ve already run out of VRAM isn’t exactly the top item on developer TODOs (who knows, maybe the memory scarcity is changing that? :P), so maybe there’s some unexplored room for performance improvements there?
Both teams worked really hard to get in the best shape. On the day of the first race, both teams were ready to win.
The green team won by one mile!
The Red team was crushed in their defeat, but they were determined to win the race next year. So they established a panel of auditors to observe the situation and ascertain if there were any differences between the teams.
After several weeks of detailed intelligence gathering, the auditors could find only one difference; the Green team had 7 rowers and 1 captain…
… and the Red team had 7 captains and 1 rower!
Un-perplexed by the raw data, upper management showed unexpected wisdom: they hired a consulting company to analyze the data and suggest a solution that would enable the Red team to win next year.
After several months the consultants came to the conclusion that the ratio of captains to rowers was the problem in the Red team. Based on this analysis a solution was proposed: the structure of the Red team has to be changed!
Like sharks getting the scent of reorganization blood, upper management wasted no time in restructuring the Red team into 4 Captains, led by 2 Managers, reporting to 1 Senior Director with a dotted line to the rower. Besides that, in a blaze of unrestricted inspiration, they suggested they might be inclined to improve the rower’s working environment by a non-monetary reward and recognition scheme if there was improved performance by the rower.
The next year, the Green team won by 2 miles…
The Red team upper management immediately fired the rower based on his unsatisfactory performance.
A bonus was paid to the Captains, Directors, and Managers for the strong leadership and motivation they showed during the preparation phase and as an incentive for them to find a better rower for the next race.
The consulting company prepared a new analysis of the restructuring activity, which showed that the strategy was good, the motivation was great, the restructuring was executed correctly, but the tool used (which was not included in the original data) was sub-standard and had to be improved.
Currently the Red team management is having a new boat designed; and to demostrate fiscal and HR dexterity for stockholders they also outsourced the rowing to India.
Using the railway network as a flatbed scanner
August 17th, 2026 — 4,600 words
Over the past few months, I’ve been working on using an industrial linear scanning camera to take very wide photos out of trains and ferries. Getting it working has been quite the challenge, but I think the results speak for themselves.
taken on the San Francisco to Oakland ferry in February 2026 (56,894x2,048 pixel grayscale image); scroll to zoom in and click and drag to move
More pictures are on display in the gallery.
I presented a talk on this project at EMFcamp 2026, which you can watch below or read on for the same story in more detail:
What am I even looking at?
The process of capturing an image like the one of the container port above.
The camera is pointed out of a moving vehicle and is constantly capturing a single vertical line kinda like these grayscale ones in the diagram, but a lot thinner. As the camera moves, what exactly it sees is changing. If I capture the lines from the camera quickly enough and stitch them together, I can produce a complete-looking image. It’s a bit more complicated than that and getting the results looking good was rather tricky, but that’s the main idea behind it.
Background and Prior Art
Back in the 1990s, digital camera sensor technology hadn’t caught up to the size and effective resolution of medium and large format film, so digital scanning backs were developed. They capture a high-resolution image without needing a giant grid of pixels by moving a single line of pixels (or three lines for color) across the frame. In the intervening years, image sensors have gotten pretty big (there’s even one that covers 4x5″ large format nowadays), but this approach is still cheaper to build for large formats than a giant sensor.
I’d been thinking about building my own digital scanning back for my large format camera for a while, but I’ve never quite gotten around to it because building something to mount properly on my camera seemed too daunting. (Buying one could have been an option, but ones from the 1990s still go for thousands of dollars on ebay and require reconstructing a computing environment of a similar vintage to use.) Late last year, I was watching a video on Gigawipf’s medium format scanning camera build and suddenly thought: “what if the entire camera moved and the subject didn’t?” and decided to give it a shot.
Loading film into my large format camera on top of a mountain in Vermont because I’m allergic to doing photography in a normal way. (The resulting pictures from that trip are here.)
I found some previous photos in the same vein (the Scannoramic project, John Hikerbiker’s experiment, Daniel Lawrence Lu’s reversal of his stationary camera, and Martin Liebscher’s very interesting film shots), but the results seemed like they could be improved upon. Surely taking the speed of motion into account and getting cleaner results wouldn’t be too hard, right?
Slit Scanning My Sofa
On the night I thought up this “big scanner” concept, I had to give it a shot. It was a bit late to go out and catch a train, so I scanned my sofa instead.
I set my phone on my office chair and slowly pushed it along as it captured a video. I then wrote some really slapdash code (which I am choosing not to share here to protect my readers) to grab the leftmost column (a “slit”) of each frame and combine them into an image.
My comments included lyrics from “Future Me Hates Me” by The Beths, which became something of a self-fulfilling prophecy when I started writing a postprocessor for the next version of the camera loosely based on that code and cursed my decisions.
It looks vaguely like my sofa, but it’s rather squished and the art on the wall is unintelligible. Surely I can do better.
I messed around with the postprocessing and doubled every column, which makes it look less squished, but it’s still a mess because I wasn’t pushing the chair at a particularly consistent speed.
I knew from the start that I’d need to measure the speed somehow, but I was naïvely hoping that I wouldn’t need to measure it that well and could simply fudge it. This image, however, shows that even small variations of speed matter. This was my first glimpse into how much of a pain dealing with speed would turn out to be.
For my next trick, I took a ride on the MBTA orange line. I taped my old phone to the seat to use its accelerometer and held my current phone to the window, making sure to turn the frame rate up all the way to 60 fps.
The accelerometer data wasn’t very useful and was even less so when I took an integral to get velocity. If I remember correctly, y was the axis of the train’s movement, but the data is so noisy that the train was apparently moving backwards at the end.
The result looks interesting, though, but I definitely need more lines if I want a properly intelligible image.
While I was getting ready for EMFcamp, I noticed another talk on the schedule by Tim Jacobs (better known online as mitxela) that was also about slit scan cameras and started to worry we’d both done the same thing. (He ran up to me after my talk to tell me he’d also worried this.) His talk started in the same way, with taking a slit from a video, but he ended up making really cool and trippy animations by going through every possible slit position for a given video.
Industrial Linear Camera
My source for more lines per second ended up being the Basler ruL2048 – 19gm, designed to be pointed at fast-moving conveyor belts. The oddly-capitalized name comes from its ability to read out its 1x2048 pixel image sensor just shy of 19,000 times per second.
These capabilities come at a price, however; brand new, the manufacturer’s lowest-spec current models go for around US$700. Thankfully for my wallet, I found mine on ebay for a tenth of that.
The price is also measured in light. since it’s capturing so quickly (the slowest exposure time is 1/100s), it needs a lot of light. I can only shoot in the daytime, and all but the brightest stations and tunnels are off limits to me.
To my surprise, having dealt with vendorware before, Basler just let me download the SDK without a support contract or proof of purchase. The most recent version also still supports this camera from 2013, which is less surprising but is still convenient.
The camera communicates with the computer over a gigabit ethernet link and the software finds it automatically as long as the relevant interface is set up for APIPA addresses (169.254.0.0/16). I could set static addresses for both ends, but I’m only using one camera at a time, so I haven’t been bothered to change it.
With surprisingly little swearing at the SDK, apart from some complaints about their use of shutter time rather than shutter speed and what a “frame” is on this camera, I put together a program that grabbed buffers of pixels and wrote them to disk.
This was my first image out of the camera using my own code, and I think it looks pretty good for just moving it freehand.
The setup and mechanical design
In order to take it on a train without needing to have three hands to hold it, I needed a way to mount it to a tripod. I ended up designing a rather utilitarian case with a heat-set insert in the bottom that my friend Brooke 3D-printed for me. Buying the parts for it gave me an excuse to finally make an order from McMaster-Carr and feel like a real engineer.
My first attempt didn’t come out because it turns out there’s these things called “manufacturing tolerances” that I completely forgot about.
Oops, that’s a bit too small.
In retrospect, I probably should’ve stuck the sensors on with something other than blue painters’ tape, but it’s held on pretty well. Going clockwise around it, the boards are:
6 degree of freedom accelerometer/gyro, which can be used with some maths to to get the speed
GPS, which didn’t end up working as well as I’d hoped because the trains in Boston are a bit too good at blocking GPS signals
SAMD21 microcontroller to shunt the data back off to the laptop
The lens on the front is a Vivitar 28mm f/2.8 that I already had for a more normal camera, with an adapter from Pentax K to the C-mount screw on the camera. Since some of the things I’m trying to shoot with it are kinda tall, its field of view worked out pretty well.
The whole thing is powered off a USB-C battery bank and there’s also ethernet and USB cables running to my laptop, so it’s a bit of a cable spaghetti monster when in action.
With the sensors attached, I could finally give them a try.
Both of these images are the same capture of waving the camera back and forth out my window, but the top one is the raw image and the bottom one is taking accelerometer movement into account. As you can see, using the accelerometer makes everything look a lot closer to normal and less stretched. (I’ll explain more of how this works in a bit in the Postprocessing Hell section.)
Boston Attempts
Once I had everything assembled, it was time to take it on a train.
I started off on the MBTA Orange Line, since it’s the closest to me, but as you can see, the results weren’t that good. Previewing what was coming out of the camera was a pain, so I kinda had to guess on the exposure, and I definitely guessed wrong. The postprocessing code I wrote didn’t work very well and everything was stretched and compressed a bit weirdly.
I went out again on a day with nicer weather and had some better luck with the exposure, although I think I messed up the focus a bit. Unlike the attempt with my phone camera, the text on station signs is pretty legible, so I’m definitely getting enough lines.
I’m particularly happy with how this one of the Longfellow Bridge from Boston to Cambridge came out. This one is in the gallery if you’d like to take a closer look.
Capture (in far too much detail)
When I was taking these early pictures in Boston, I was using a tool from the camera vendor called Pylon to preview. The black horizontal section was all I could see of the image at any one time, and it’s rotated 90° from how I’d like to see it. Dialing in the exposure in it, releasing its grip on the camera, and then starting my own code back up before the train started moving again was a right pain that I had to do something about.
My first attempt at a GUI of my own used OpenCV highgui, which didn’t really work for this. It requires a 1 ms delay after each frame, which is fine for slower cameras, but would cause me to miss 4 entire lines (250 μs each at the shutter speeds I’m usually using) every display frame (256 lines).
I ended up using Dear ImGUI instead, which worked nicely with the frame acquisition loop I already had. Out of the approximately two dozen backends the library supports, I picked GLFW (“girl love for workgroups”, to quote a message from a friend at the time) and OpenGL3, probably because of the “girl love” quip, although I’m not certain.
I wrote most of the GUI in a single sleepless night in Toronto where rotating the image felt like the single hardest problem in computer science. (There’s definitely a few things I can do to improve the implementation I settled on, but it runs well enough for the time being.) Unfortunately, the pictures I took in Toronto didn’t really come out, but at least they were exposed correctly.
I encountered some strange bugs while adding a histogram for the image.
Getting the accelerometer data proved to be something of a pain. my first version sent readings as text over serial, which turned out to be very computationally intensive on the microcontroller. (Converting floating point numbers to strings and then assembling strings is very expensive, even on a relatively powerful SAMD21 microcontroller that has thirty-two entire bits.) I decided to move the conversions over to my laptop, which has the processing power to handle them with ease, but this came with problems of its own.
A very frustrating debugging session.
The accelerometer measurements were sent as raw floating point numbers, but GPS data was still in NMEA sentences and switching between them required sending fixed byte sequences and hoping that nothing got misinterpreted as those sequences. (Nothing in a NMEA sentence should come across as 0x11 0x11 0x11 0x11, my accelerometer data start sequence, but it’s not completely impossible for accelerometer data to contain 0x22 0x22 0x22 0x22, my NMEA string start sequence.)
I also ran into issues where not flushing the serial port at the right time ruined an entire day’s shots. Thankfully, I was capturing on the Mattapan Line in Boston, and I can pretty easily go back and try again. That “seam” in the image is where it lost all serial data for around half a second, which is an eternity in line camera time. The software kept waiting for another accelerometer sample that never came because the serial port buffer was full.
See It, Say It, Sorted
The fully assembled camera looks like a suspicious mess, and the witch using it doesn’t look much less so.
The camera isn’t usually held together with this much tape, but I’d forgotten to bring the tripod mount plate on that trip to Montréal. Would you trust her to bring strange equipment onto your train?
Despite Boston’s history of police overreaction to harmless electronics projects, I worry the least about being arrested on the MBTA. People here tend to mind their own business and have never called the cops on me. The police also don’t ride the trains much, preferring to harass people in stations instead.
I’m less used to how things work in other cities, so I only take the camera out when riding with a friend to look out for trouble (and sometimes to listen to dispatch radio).
So far, I’ve only been seen, not said or sorted. I’m crossing my fingers that this doesn’t change as I take the camera more places.
On my trip to Montréal, I was stopped by security in Gare Centrale and informed that tripods weren’t allowed and asked, au franglais, whether I was recording or taking a picture. Rather than try to answer that philosophical question in a language I don’t speak, I just said “désolé” a few times and put away the tripod, which seemed to be sufficient.
The pictures I took in Montréal are here in the gallery (images 2 and 3) if you’d like to see them.
Postprocessing Hell
Capturing image and accelerometer data turned out to be the easy part compared to postprocessing and making the images actually look good.
The camera captured somewhere around 4,000 lines per second, so I had more lines than I needed in every capture and had to pick which ones actually matter.
What happens if I take too few lines (Autoroute 10 in Brossard, Québec out of the window of the REM A) Jumping between lines too quickly looks artificial and wrong, like is visible at the waterline in this album cover edit of an early version of the Oakland ferry photo.
To decide which lines to use, I ended up using the speed, as measured by an accelerometer, but this came with several problems.
Firstly, accelerometers don’t actually measure speed. They measure acceleration, the rate of change of velocity. By taking an integral, I can get velocity, but that’s relative to an initial value. I can usually assume that the starting speed is at a station and is thus zero, but I can’t be certain of that. If it isn’t zero, I have no good way of knowing the correct value and just have to guess until I find one that smells right.
Secondly, as shown in this diagram, the accelerometer I’m using is only measuring so quickly. The camera is grabbing lines maybe 4 times faster than it, so every few lines have to share a speed value. It also isn’t very consistent because my microcontroller code isn’t as fast as it could be, so this could cause irregularities in the final image. How many acceleration measurements there are or aren’t also changes how accurate the integral is, which creates more problems.
You might remember that I mentioned putting a GPS receiver on the camera earlier, and while I did do that, it wasn’t very useful. It didn’t get a signal on most of the trains I tried it on, and when it did manage to get one, it only read 10 times a second, which covers 400 entire lines out of the camera. If it worked a bit more consistently, it could be useful for correcting for integration error using a Kálmán filter, but that’s a problem for when I have better GPS data.
Even if my speed measurement is perfect, I still have the problem of parallax, where things closer to the camera appear to move faster than things further away. This is independent of optical focus, which I usually set at infinity.
-u 10 distance units per pixel
This problem can be dealt with by changing how much distance each pixel represents. Lower values emphasize things closer to the camera more, while higher ones make the background more visible. You can give this a try by moving the slider!
Each of these images is same size (10,000 pixels wide by 2048 tall, scaled to fit your browser) and each includes everything from the previous by virtue of covering more of the capture. The units are arbitrary and don’t measure real distance (I could make it actual meters per pixel, but I don’t see a point to that.)
The camera and software have no idea what I want to “focus” on, so I make the artistic decision and manually pick that for each segment of the image and stitch the segments together to get the pictures in the gallery. I tested different values for distance per pixel and starting velocity of each segment and then stuck them together in GNU IMP to produce the final images. The assembled images often became too big for the 65,535x65,535 maximum size of a JPEG file, so I used the good old TIFF format. (The PNG specification allows similarly large images in theory, but the software I had to hand seems to like big TIFFs better than big PNGs.)
Notes on distance per pixel (u) and starting velocity (v) values for each part of a few images
The program that takes the accelerometer data into account for every line of the image is called grindstone, since it grinds multi-gigabyte raw captures down into smaller usable images. My first version was loosely based on my very bad slit scan code from earlier and was extremely slow, taking hours to capture a minutes-long capture. It would often fail to save after running for hours because the resulting image was too big for the JPEG format, and debugging it was an absolute pain.
I ended up nerdsniping my friend Maddie into rewriting grindstone in idiomatic NumPy, to make the mathematical operations that were going on clearer (she insists that all the operations were already in the original, and her changes were along the lines of “transforming it into a magical girl”). Maddie would later split this version into a “perhaps slightly overengineered” pipeline of several different stages, making it easier to experiment, and swap in different operations, output strategies, and the like. Thanks to her help, I’ve been able to try different combinations of parameters much more easily, and get results I’m much happier with.
Color Hell
In April, my friend Ari and I went for a ride on the Mattapan Line as the leaves were coming in on the trees. The pictures I took didn’t come out due to a capture software bug (see Capture) and I haven’t gotten around to going back yet, but it left us with the thought that color line cam photos might look cool, especially in autumn.
While browsing ebay late one night, I found a very good deal on a color line camera of the same generation as the monochrome one I already had (the Basler ruL2098 – 10gc, 3x2098 pixels at around 10,000 lines per second). After a bit of disassembly (it came to me in the housing it was used in on some factory line) and swapping the lens mount over, the camera was ready mechanically.
I ended up putting red, green, and blue stripes on it so I could tell the cameras apart without taking the lens off or squinting at tiny text on the label.
The capture software side wasn’t that much harder, although I did have to fix a bunch of assumptions about the size of each line and redo the rotation for the GUI
Progress of getting the color capture working
Thanks to the very modular way that Maddie rewrote grindstone, adding support for color images wasn’t too difficult, although we did have to fix some strange-looking bugs.
The train was moving so slowly and inconsistently in this picture that integration error piled up and grindstone calculated that the camera was moving backwards and jumped to various previous points in the capture.
With capturing and processing images mostly working, more problems became apparent. The most visible one is that leaves are all far brighter than they should be. This happens because the color camera is sensitive to infrared light on all three channels. (If it was only sensitive to it on the red channel, the leaves would look reddish, but the combination of all three channels’ IR with the strong visible green leads to the greenish white in this picture.) The monochrome camera is sensitive to IR too, but it doesn’t matter because it’s just one channel and visible light completely drowns it out.
(Diagram taken from the camera’s manual)
I will admit the effect does look pretty good in the right light. This picture taken in Manchester-by-the-Sea, north of Boston, is both grayscale and colorful at once. (Read on to learn what the color fringes in the background are.)
I solved this with an UV and IR cut filter that only passes light between 400 and 700 nm, which is close enough to the human visible spectrum that everything looks right. This is the first big capture I took with it, and I only needed to adjust the colors minimally in post.
I also tried a filter that only passes light longer than 720 nm (I’ve had quite interesting results with it and IR-sensitive film), and I’m definitely going to try taking more pictures with it in the future.
The next problem is that some things end up with weird red, green, and blue fringes, especially subjects that are further from the camera or moving faster. They turn out to be inherent to how this camera sensor works. Red, green, and blue are each separate vertical lines (instead of a Bayer filter), and thus can’t see exactly the same thing at the same time. The fringes come from when just one line sees something, and it’s particularly noticeable with bright subjects. They’re diagonal and not perfectly vertical because the camera itself isn’t perfectly vertical. (I try to get it close, but there’s only so much I can do on a moving train.)
From the camera’s manual; the manufacturer provides formulas that can be used with the optical magnification factor of the lens and the exact speed to counteract it, but I don’t have (relative) speed estimates for the subject.
I correct for it for a given subject by shifting the red and blue channels to line up with the green channel. Since the lines are evenly spaced, I can shift by the same amount in opposite directions rather than having to measure separate offsets for each channel. In theory, I could decide how far to shift by correlating brightness shifts across channels, but at present, I do it manually. Separation between channels is visible on the sailboat’s masts, and I corrected for it by shifting the red channel 10 pixels right and the blue channel 10 pixels left. Color fringes are still visible in the background because it’s much further away than the sailboat and thus has a faster angular velocity; I could shift and correct for it, but the sailboat would look much worse.
For nearly 16 years, our mission has been simple: to prove that a smartphone can be made differently. We’ve focused on creating technology that works well, lasts longer, and is made with genuine care for the people and materials behind it. Last year, we brought that vision to life with the Fairphone (Gen. 6), our clearest expression yet of what a responsible phone should be.
But we also listened to your feedback. You asked for more performance, more memory, and more personalization. Today, we are answering that call.
Today, we launch the Fairphone (Gen. 6+).
It is the Fairphone you already know, but better where it matters most.
More power. More performance.
The Fairphone (Gen. 6+) comes with upgraded internal hardware that doesn’t compromise on our industry-leading repairability standards. The ‘plus’ isn’t about adding features for a longer spec sheet; it’s about intentional improvements that keep your device capable for longer.
- The Fairphone (Gen. 6+) is powered by the Snapdragon® 7s Gen 4 processor, going up a generation compared to its predecessor.
- We’ve also increased the memory from 8GB RAM to 12GB DDR5 RAM. That’s the most we’ve ever put in a Fairphone!
Together, the combination gives the new Fairphone a significant boost in performance with smoother multitasking and a more fluid user experience, now and in the future. App loading and switching speeds are faster by up to 24% while system RAM reservation time is down by up to 20%. You’re also getting Android 16 out of the box, with the promise of six OS upgrades over its lifetime.
It’s everything you love. Plus more.
The Fairphone (Gen. 6+) builds on what made the Fairphone (Gen. 6) an award-winning bestseller for us. That’s also why we have also retained a lot of what made the original so good.
- Industry-leading warranty of up to five years.
- Software support guaranteed through 2033.
- Modular and repairable design with 12 user-replaceable parts.
- 256GB on-board storage (expandable upto 2TB with microSD)
- Triple-camera system with 50MP Sony Lytia 700c main camera
- 6.3” LTPO OLED Display
- Up to 53 hours of battery life on a single charge
- IP55 dust and water resistance
New edition, new color
The new Fairphone comes in Horizon Black and Forest Green, along with an exclusive Cobalt Blue colorway that’s more than a style statement. When we introduce a new color, it’s never just a design decision. The reason we zeroed in on Cobalt Blue is because it serves as a nod to our origin story as a conflict mineral awareness campaign and our commitment to fairly sourced raw materials.
Having said that, it’s quite the bold look that’s sure to make heads turn and get conversations going. And even better, you can make sure your accessories match as well, with the card holder, finger loop, and protective cover available in the new color as well. And yes, it’s still as fair as ever: it contains 51% fair and recycled materials (by total weight), supports our living wage bonus program, is assembled under fair working conditions using renewable energy, and is 100% e-waste neutral!
Already own the Fairphone (Gen. 6)?
Updated Fairphone Moments: Our dedicated digital minimalist mode now offers even more control and more customization. Add new wallpapers, reorder your preferred apps, map new functionalities to the Switch button, switch on blue filter mode; there’s a lot more you can do with Fairphone Moments.
New Fairphone Gallery: You’ve been asking for this for a long time. Our new local gallery app allows you to store your photos and videos locally without depending on third-party services or paid cloud storage.
The best part? These software updates are also coming to the Fairphone (Gen. 6). It just wouldn’t be fair otherwise.
Hello, America!
This launch marks a massive milestone for us. Fairphone is now officially available in the United States. For the first time, sustainable tech enthusiasts across the pond can purchase a fully unlocked Fairphone (Gen. 6+) along with the Fairbuds and Fairbuds XL through our dedicated US web shop. And with the number of comments we see on our socials, this has been a long time coming!
Discover the Fairphone (Gen. 6+) today: Shop EU | Shop US*These figures reflect internal laboratory testing using SmartViser diagnostic tools under conditions designed to approximate real-world use. Actual performance may vary depending on factors such as device configuration, network conditions, and usage patterns.
®Snapdragon and Qualcomm branded products are products of Qualcomm Technologies, Inc. and/or its subsidiaries.
In 2023, I was in need of a new laptop that should hopefully last me for a while. While looking at my options, I was seduced by Framework’s promise of a repairable and upgradable laptop that supports Linux out-of-the-box without weird driver issues, as well as the option to assemble the laptop myself1 and buy the RAM and SSD I want separately2, back when they were cheap.
For three years, the laptop has served me well, until Framework suggested via newsletter to install the latest BIOS3 update, version 3.20, with a bunch of security fixes. Unfortunately, the system hung and displayed a corrupt image on the screen, signifying a failed BIOS flash.
Naturally, I reached out to Framework support, who told me to unplug the laptop, let the battery drain, and power it back on again afterwards, hoping the laptop would recover by itself. Unfortunately, it never did, and after giving Framework a bunch of information, they informed me that since my 1-year warranty has expired, I have no option but to purchase a new Framework motherboard, which will cost at least CA$500.
A quick search revealed that many people had issues with BIOS flashes with this specific BIOS update on the Framework forums, even those in warranty, and on a different thread, people have been having similar issues with BIOS flashing in general on this model since at least March of 2025. To my knowledge, Framework has never acknowledged the issue or offered any indication that the problem was fixed, so buying a new motherboard would simply be playing Russian roulette if I ever wanted to update the BIOS again, on top of spending CA$500+ through no fault of my own.
Thus, I opted against buying a new motherboard and embarked upon a journey to flash the BIOS myself. I documented this journey in excruciating detail so that hopefully, by following along, you’ll understand exactly how you might fix similar problems.
Table of Contents
Why Framework?
The fatal BIOS flash
Reaching out to support
Troubleshooting on my own
The BIOS chip
The flash programmer
Purchasing the tools
Response from Framework
The data breach
Extracting the BIOS image
Delivery of the tools
Connecting the chip
Executing the flash
Consequences of flashing
Conclusion
Why Framework?
In 2023, my basic requirement for a laptop was as follows:
Compatible with Linux;
Small and light enough for travel;
A standard US keyboard layout, not that horrible Canadian Multilingual Standard layout that’s somehow very common in Canada4;
A modern CPU, but not necessarily super high core count, as I don’t intend to do intensive compiling or gaming while travelling; and
Socketed RAM and SSD, so I can upgrade those down the line, or buy from a third party if it made financial sense. I wanted to start it at 32 GiB of RAM5 and 1 TB of SSD6, since those were reasonably affordable in 2023.
As such, my options are effectively limited to the 13” thin-and-light laptops without a discrete GPU. At the time, AMD Ryzen was ahead of Intel in the performance department, so I decided to go for an AMD CPU.
There were a plethora of 13” thin-and-light AMD Ryzen laptops that fit the bill, but since I didn’t need the new laptop right away, I figured I might as well try something new.
At the time, Framework was a relative newcomer on the laptop scene, promising a repairable and upgradable experience, along with swappable ports. I rather liked the idea of not being locked to the ports that the manufacturer decided to put onto the laptop, and I wanted more upgradability also. It definitely helped that Linux came with full first-party support and no requirement to run patched kernels or anything crazy like that.
Furthermore, Framework was a very big proponent of the right to repair movement, and I strongly believe that laptops should be repairable, like desktops, and not just thrown away after a minor problem, so I also wanted to support them on that front.
So I looked at the price premium for Framework, and it wasn’t actually that much more expensive once I opted for the DIY edition, sourced my own RAM and SSD, and skipped the pointless Windows licence. With another laptop brand, I would have to either buy the model with the lowest RAM and SSD and upgrade it to the 32 GiB of RAM and 1 TB of SSD that I wanted, or pay a premium for the manufacturer to put those in.
So I decided to just go for it and pre-ordered a Framework laptop, and it finally arrived a few months later to much anticipation. I simply slotted in the RAM and SSD, connected the input cover, screwed it in, installed the bezels, and that was it. It honestly felt a bit anti-climactic for a DIY laptop. I then put in a Debian netinst USB drive, and I was off to the races.
For the next three years, I actually had a relatively nice experience, and the AMD Ryzen 5 7640U with Radeon 760M Graphics was still plenty fast for what I needed the laptop to do. There was definitely no need to upgrade, though I could, in theory.
The fatal BIOS flash
For the longest time, Framework appeared to be a very consumer-friendly company, providing regular BIOS updates and an easy way to install them on Linux through the Linux Vendor Firmware Service (LVFS) and fwupd. In fact, I am subscribed to Framework’s newsletter, which informs me of any updates coming out.
I’ve done many BIOS updates on Framework through fwupdmgr update, and save for the annoyance of rebooting the laptop and waiting like ten minutes for the BIOS updater to finish flashing, nothing bad has ever happened.
On July 7th, 2026, Framework sent me the following email:
From: Framework <support@frame.work> Subject: Software update for your Framework Laptop 13 (AMD Ryzen™ 7040 Series) - BIOS 3.20
We have a BIOS update for your Framework Laptop 13 (AMD Ryzen™ 7040 Series). We recommend always installing the latest version of BIOS and drivers to keep your system secure, stable, and running at high performance.
BIOS 3.20, with updaters for Windows and Linux
Added support for Framework Laptop 13 Pro features - Enabled compatibility for the haptic touchpad, touch panel, and 74W battery. Updated the audio verb table to support the new speakers in the Framework Laptop 13 Pro chassis. Updated AMD PhoenixPI-FP8-FP7_1.2.0.0f. Fixed an issue where the system was unable to boot from partially locked self-encrypting drives (SEDs). Fixed an issue where the Battery Extender status was reported incorrectly following a reboot, hibernation, or shutdown after the timer had expired. Fixed an issue where the system boots with black screen when a Dell U2725QE monitor and a mouse were connected. Supported 16bits postcode. Fixed an issue where system audio volume was lower on 3.19 beta. Security fixes
CVE-2025 – 54502 - CVSS score N/A. CVE-2025 – 29949 - CVSS score N/A. CVE-2025 – 0040 - CVSS score N/A. CVE-2024 – 36355 - CVSS score N/A. CVE-2024 – 36310 - CVSS score N/A.
You can learn how to check your current BIOS version, see the full details on the updates, and always get access to the latest software on the Framework Laptop 13 (AMD Ryzen 7040 Series) downloads page.
From: Framework <support@frame.work> Subject: Software update for your Framework Laptop 13 (AMD Ryzen™ 7040 Series) - BIOS 3.20
We have a BIOS update for your Framework Laptop 13 (AMD Ryzen™ 7040 Series). We recommend always installing the latest version of BIOS and drivers to keep your system secure, stable, and running at high performance.
BIOS 3.20, with updaters for Windows and Linux
Added support for Framework Laptop 13 Pro features - Enabled compatibility for the haptic touchpad, touch panel, and 74W battery. Updated the audio verb table to support the new speakers in the Framework Laptop 13 Pro chassis. Updated AMD PhoenixPI-FP8-FP7_1.2.0.0f. Fixed an issue where the system was unable to boot from partially locked self-encrypting drives (SEDs). Fixed an issue where the Battery Extender status was reported incorrectly following a reboot, hibernation, or shutdown after the timer had expired. Fixed an issue where the system boots with black screen when a Dell U2725QE monitor and a mouse were connected. Supported 16bits postcode. Fixed an issue where system audio volume was lower on 3.19 beta. Security fixes
CVE-2025 – 54502 - CVSS score N/A. CVE-2025 – 29949 - CVSS score N/A. CVE-2025 – 0040 - CVSS score N/A. CVE-2024 – 36355 - CVSS score N/A. CVE-2024 – 36310 - CVSS score N/A.
Added support for Framework Laptop 13 Pro features - Enabled compatibility for the haptic touchpad, touch panel, and 74W battery.
Updated the audio verb table to support the new speakers in the Framework Laptop 13 Pro chassis.
Updated AMD PhoenixPI-FP8-FP7_1.2.0.0f.
Fixed an issue where the system was unable to boot from partially locked self-encrypting drives (SEDs).
Fixed an issue where the Battery Extender status was reported incorrectly following a reboot, hibernation, or shutdown after the timer had expired.
Fixed an issue where the system boots with black screen when a Dell U2725QE monitor and a mouse were connected.
Supported 16bits postcode.
Fixed an issue where system audio volume was lower on 3.19 beta.
Security fixes
CVE-2025 – 54502 - CVSS score N/A. CVE-2025 – 29949 - CVSS score N/A. CVE-2025 – 0040 - CVSS score N/A. CVE-2024 – 36355 - CVSS score N/A. CVE-2024 – 36310 - CVSS score N/A.
CVE-2025 – 54502 - CVSS score N/A.
CVE-2025 – 29949 - CVSS score N/A.
CVE-2025 – 0040 - CVSS score N/A.
CVE-2024 – 36355 - CVSS score N/A.
CVE-2024 – 36310 - CVSS score N/A.
You can learn how to check your current BIOS version, see the full details on the updates, and always get access to the latest software on the Framework Laptop 13 (AMD Ryzen 7040 Series) downloads page.
However, I suspected that flashing a BIOS right away might not be a good idea, given the potential for bugs, so I decided to wait for a bit. I figured that if there were problems, either a new update would be released, or the update would be pulled. Seeing neither, I finally decided to do a quick flash in the morning of August 5th, while I cooked breakfast.
However, when I came back, I saw this screen, and instantly knew something had gone horribly wrong:
Framework BIOS flasher showing a triangle and diagonal patterns it’s not supposed to show7
Given that the BIOS flasher was stuck and probably rendering random stuff from memory to the screen, I have no choice but to conclude that the BIOS flash had failed.
Still, I left the laptop for a few hours, just in case it decided to recover. It never did.
Reaching out to support
Naturally, I reached out to Framework support, hoping for a quick response and a solution to my problem:
Subject: Stuck on BIOS Update Support Request Category: Problem with my Framework Product Was your Framework Product Delivered within the last 30 days?: No it wasn’t Product: Framework Laptop 13 Framework Laptop 13 Generation: AMD Ryzen 7040 Series Operating System: Linux Linux Distribution: Debian 12 [typo, should have been 13] BIOS: 3.20 Order number: [redacted] Product Issue Selection: Mainboard Description: I tried to update the BIOS with fwupdmgr update, and upon reboot, the system is stuck in this weird state and not making any progress for over an hour at this point. It’s not displaying properly, see picture. What do I do now?
Subject: Stuck on BIOS Update Support Request Category: Problem with my Framework Product Was your Framework Product Delivered within the last 30 days?: No it wasn’t Product: Framework Laptop 13 Framework Laptop 13 Generation: AMD Ryzen 7040 Series Operating System: Linux Linux Distribution: Debian 12 [typo, should have been 13] BIOS: 3.20 Order number: [redacted] Product Issue Selection: Mainboard Description: I tried to update the BIOS with fwupdmgr update, and upon reboot, the system is stuck in this weird state and not making any progress for over an hour at this point. It’s not displaying properly, see picture. What do I do now?
I’ve also attached that picture of the screen above.
Support did not respond until one day and 8 hours later.
Troubleshooting on my own
In the meantime, I figured that letting the computer hang indefinitely—especially with the CPU fan spinning loudly—wasn’t the best idea, so I decided to do some research. It wasn’t very long before I came across this thread, with a bunch of people having the same problem doing the same update to BIOS 3.20 from 3.18, just like I did, though I saw a slightly different screen.
Users on the thread who were under warranty reported getting their motherboard replaced, while those out of warranty reported Framework offering zero help. This was very concerning to me.
Seeing on that thread that support recommended that people in a similar situation unplug the charger and let the battery drain until the laptop eventually powers off, I did exactly that, while diving deeper on the forums to see what was in store for my future.
I then came across this other thread, wherein the forum user @cesfahani, who saw the exact same screen I did, detailed how they used their Raspberry Pi and soldering skills to flash the BIOS chip externally. While I could do some basic soldering, as seen when I built my stratum 1 NTP server8, I was not prepared to solder tiny wires to a tiny BIOS chip.
It soon became apparent to me that if BIOS flashing on Framework fails and it doesn’t automatically recover by itself, there was no recovery mechanism short of externally programming the BIOS chip. This was shocking on a product advertised as “repairable.”
I couldn’t help but remember my first PC, secondhand as it was, with the 2004-vintage P4P800 SE motherboard. I still remember reading the manual from front to back, as an excited child with zero desire to break my very first PC. Even the 22-year-old motherboard had the ASUS “CrashFree BIOS 2” feature, which was advertised to fix a bad flash without resorting to such crazy manual methods. I’d simply have to put in a floppy disk9 or a CD with a BIOS image named P4P800SE.ROM after a bad flash, and it would automatically recover. Yet, here I am decades later, dealing with a “repairable” laptop without such a feature.
I also wondered whether such a thing was specific to laptops, so I did a quick search on whether the brands that I didn’t choose back then, like Dell and HP, supported such recovery features. For example, Dell laptops could recover the BIOS from USB or the recovery partition after holding down Ctrl+Esc while plugging in the power, and HP laptops have a similar “HP Sure Start” feature that recovers the BIOS. So Framework is actually doing worse than their “not-repairable” competitors on this front.
Fortunately, reading further down the thread offered a glimmer of hope: Instead of soldering tiny wires to the chip on a Raspberry Pi, forum user @moparisthebest revealed that I could use something called “pogo pins” connected to a USB flash programmer to do the job without any soldering. Even further down the thread, users @David_Henry and @Richard6 reported success doing something similar.
The BIOS chip
Before we go any further, it is important that we first understand the BIOS chip that we are dealing with. Otherwise, talks of flashing it would just be a confusing mess of jargon, which was my experience when first reading the thread.
The flash chip in question is located to the right of the M.2 slot, hidden under a plastic cover, and it looks like this (rotated 90° clockwise to make the label upright):
The BIOS chip in question, with the M.2 slot for scale
As you can see, this is a Winbond 25R256JWEQ chip. I found the datasheet for the W25Q256JW series, and discovered the whole series to be 1.8 V, 256 M-bit (i.e. 32 MiB) SPI flash chips. It will be very important to find a BIOS image for this motherboard that is exactly 32 MiB, then flash it at exactly 1.8 V to avoid destroying it.
What’s SPI? It’s a de facto standard called the Serial Peripheral Interface, commonly used in embedded systems for communication between integrated circuits. This standard is why the Raspberry Pi could talk to and flash the chip, as could many microcontrollers.
There are several variants of the W25Q256JW chip, differentiated by form factor:
the P variant, which is an 8-pad, WSON 6×5 mm chip;
the E variant, which is an 8-pad, WSON 8×6 mm chip;
the F variant, which is a 16-pin SOIC 300-mil chip; and
the B and C variants, which are ball grid array chips.
We have the E variant here, which means it’s a WSON 8×6 mm chip. From the datasheet, we can see its schematic:
Pinout schematic for Winbond 25R256JWEQ chip
It’s also very important to note the white dot on the top-left corner of the chip, as shown in the picture and on the pinout schematic. That dot is placed next to pin 1 of the chip, allowing it to be oriented. Very bad things will happen if you rotate the chip the other way and connect VCC to GND instead.
Could how much sugar you consumed before your second birthday shape your health decades later — even into your 70s?
A new study that my colleagues and I conducted suggests the answer could be yes. People who had less sugar exposure during the first few years of life were much less likely to develop several different cancers later in life, and they also showed signs of slower biological ageing. We also found that even in adulthood, they continued to consume less sugar and had healthier diets overall.
This is not an easy question to study in real life. Researchers obviously can’t run that experiment on real babies — split them into groups, feed one group sugar, then wait 70 years to see what happens.
Britain’s postwar sugar rationing offered a rare alternative. After the second world war, sugar remained rationed in Britain for several years. But in September 1953, rationing ended and people started eating a lot more sugar. Consumption nearly doubled. This meant that children born just a few months apart had very different levels of sugar exposure both in the womb and during early childhood.
For our study, we used data from over 64,000 people born in Britain between 1951 and 1956. Those born earlier spent a larger share of their first 1,000 days under sugar rationing, while those born later experienced progressively less of it.
By the first 1,000 days, we mean the period from conception to a child’s second birthday. This is a critical stage because the body is developing very rapidly. Organs are growing, metabolism is taking shape, and the immune system is maturing.
Food and taste preferences also begin to form during this period. Although 1,000 days is a relatively short period, the effects of the food environment during this stage can last throughout a person’s life.
To track cancer cases, we used the UK Biobank — a huge health database that follows participants for decades and records, among other things, who develops cancer and when.
We found that people who experienced longer periods of sugar rationing during their first 1,000 days had a lower incidence of five types of cancer: breast, prostate, liver, rectal and lung cancer. The effect was strongest for liver cancer, where rates were around 69% lower, and weakest — though still substantial — for breast cancer, at 36% lower. Importantly, these differences only began to appear decades after sugar rationing had ended.
We also found that exposure to sugar rationing is related to biological ageing. Biological age is not the same as chronological age. Two people may both be 70 years old, while their cells and immune systems show different levels of ageing.
One way we measure biological ageing is through telomere length. Telomeres are protective structures located at the ends of chromosomes. As cells age, telomeres usually become shorter.
We found that people who experienced longer periods of sugar rationing during their first 1,000 days had longer telomeres. Based on our estimates, this difference is equivalent to about 2.2 years of slower biological ageing.
We also found lower levels of a protein called granzyme B, which rises when the immune system has been working overtime for years — another sign of slower ageing at a cellular level. Taken together, these results suggest that lower sugar exposure early in life may be related to slower ageing at the cellular level.
The sugar habit that lasted 50 years
What is most surprising, however, is not the cancer results or the biological markers of ageing. It’s how early-life sugar exposure continued to affect people’s diets even 50 years later.
We found that people who experienced sugar rationing early in life still consumed less sugar in adulthood, around age 50. They also ate less overall, and their diets were healthier and more diverse. This suggests that the level of sweetness people are exposed to very early in life may shape their taste preferences for a long time.
There appear to be two mechanisms at work. One is biological — early nutrition may shape how the metabolism, organs and immune system develop. The other is behavioural — a taste for less sweetness, formed early, seems to stick.
Other research using the end of Britain’s sugar rationing has also found that people who experienced more sugar rationing early in life had lower risks of type 2 diabetes and hypertension. A recent study using the same end of Britain’s sugar rationing found lower risks of dementia and Alzheimer’s disease, while another study reported lower risks of heart disease and stroke.
This does not mean that children should not eat sugar at all, and it certainly does not mean that postwar sugar rationing is a desirable policy. What this historical evidence shows is that, during a critical period of development, even relatively small differences in sugar exposure over a short period may leave effects that are still observable half a century later.
Children may not remember what they ate before age two. But their later health, their bodies and even their taste preferences may still carry the imprint of those very early-life experiences.
To add this web app to your iOS home screen tap the share button and select "Add to the Home Screen".
10HN is also available as an iOS App
If you visit 10HN only rarely, check out the the best articles from the past week.
Visit pancik.com for more.