HNHacker News
TopNewBestAskShowJobs

exDM69

10,584 karma · joined February 2, 2010

submissionscomments
exDM69··on How to create an OS from scratch
> hobby wheel reimplementation projects like this, I find doing it as close to the bare metal as possible, relying on the minimum amount of other people's code, a lot more fun.

As a chronic wheel reinventor, I can understand this.

But if I felt like scratching this itch now, I would pick some other hardware than x86. The RP2040 could be a nice target, or maybe some ARM or RISC-V SoC.

That said, I totally understand that there's something different to having a bare metal / hobby OS project running on your daily driver computer than some embedded gadget.

I'm speaking from experience here, I've written bare metal projects in the style of this project (first project I did with MS-DOS debug.com, wrote to a floppy disk and rebooted my machine from the floppy) and the "modern" way with compilers, emulators, debuggers etc. The difference in productivity and learning the interesting stuff is huge.

exDM69··on How to create an OS from scratch
Having built a few bare metal and hobby OS projects over the years, I would not recommend the path taken in this tutorial series.

Because if you want to write an OS, don't write a bootloader first. This article essentially describes a stage 1 bootloader on top of legacy BIOS firmware. It will teach you about historical x86 minutiae, which is nothing but a hindrance if you want to understand OS concepts.

Instead you should try to get a bootable ELF (multiboot) or PE (UEFI) image written in a high level language (C, C++, Rust, Zig, etc) as soon as possible. You can boot it up in QEMU very easily (compared to a boot sector image) and get a real debugger (gdb), system monitor and all the other invaluable tooling up. This will greatly affect the velocity of your project and get to the interesting stuff faster.

Because bare metal/OS projects may be hard to write but they are even harder to debug. You will need all the help you can get.

exDM69··on AMD Open Source Driver for Vulkan project is discontinued
> I wish hardware vendors would just stop trying to write software.

How would/should this work? Release hardware that doesn't have drivers on day one and then wait until someone volunteers to do it?

> Intel had great success for many years by doing that well

Not sure what you're referring to but Intel's open source GPU drivers are mostly written by Intel employees.

exDM69··on In Defense of C++
> How exactly does your debugger know whether the compiled code it stepped into came from C++ or Fortran source?

Executables with debug symbols contain the names of the source files it was built from. Your debugger understands the debug symbols, or you can use tools like `addr2line` to find the source file and line number of an instruction in an executable.

Debugger does not need to understand the source language. It's possible to cross language boundaries in just vanilla GDB for example.

exDM69··on Shipping textures as PNGs is suboptimal
GPU texture compression needs fixed bitrate for random access, usually 2 bits per pixel (128b for 8x8 block) with modern formats.

Jpeg or other variable bitrate compression schemes are not applicable.

exDM69··on Shipping textures as PNGs is suboptimal
It's an open ended optimization problem. You can get bad results very quickly but on high quality settings it can take minutes or hours to compress large textures.

And the newer compression formats have larger block sizes from 8x8 to even 12x12 pixels. ASTC and BC7 are a different beast to the early texture compression formats. You can get pretty awesome image quality at 2 bits per pixel (and slightly less awesome at less than a bit per pixel).

exDM69··on Shipping textures as PNGs is suboptimal
Note that KTX2 is a container format. It can contain uncompressed or compressed texture data. Some of the tools you mention might support "KTX2" but they won't compress textures for you, they just put the uncompressed data in a KTX2 container.

And my out-of-the-box Gimp 3.0.4. doesn't have KTX export at all. Didn't check the other tools you mention.

exDM69··on Shipping textures as PNGs is suboptimal
Not for multi-channel SDF at least. Texture compression works terribly badly with "uncorrelated" RGB values as they work in chroma/luminance rather than RGB. For uncorrelated values like normal maps, there are texture compression formats specifically for that (RGTC).

However, your typical MSDF font texture has three uncorrelated color channels and afaik there isn't a texture compression format with three uncorrelated channels.

exDM69··on Video Game Blurs (and how the best one works)
Has anyone got suggestions for blur algorithms suitable for compute shaders? The usual Kawase blur (described in the article) uses bilinear sampling of textures.

You can, of course, implement the algorithm as is on a compute shader with texture sampling.

But I have a situation where the inputs and outputs should be in shared memory. I'm trying to avoid writing the results out to off-chip DRAM, which would be necessary to be able to use texture sampling.

I spent some time looking into a way of doing an efficient compute shader blur using warp/wave/subgroup intrinsics to downsample the image and then do some kind of gaussian-esque weighted average. The hard part here is that the Kawase blur samples the input at "odd" locations but warp intrinsics are limited to "even" locations if that makes sense.

I would appreciate if anyone knows any prior art in this department.

exDM69··on Making Minecraft Spherical
The article doesn't describe the way to avoid the difference in rectangle size in a cubesphere, so let me.

The bad way: - Generate a cube - Subdivide each face using linear interpolation (lerp) - Normalize each vector to put it on a unit sphere

The good way: - Generate a cube - Subdivide using spherical linear interpolation (slerp) - done!

The cubesphere has lots of interesting geometric properties, particularly in texture mapping.

exDM69··on Malicious versions of Nx and some supporting plugins were published
The system package manager and the language package/dependency managers do a very different task.

The distro package manager delivers applications (like Firefox) and a coherent set of libraries needed to run those applications.

Most distro package managers (except Nix and its kin) don't allow you to install multiple versions of a library, have libs with different compile time options enabled (or they need separate packages for that). Once you need a different version of some library than, say, Firefox does, you're out of luck.

A language package manager by contrast delivers your dependency graph, pinned to certain versions you control, to build your application. It can install many different versions of a lib, possibly even link them in the same application.

exDM69··on Malicious versions of Nx and some supporting plugins were published
The same applies to any Makefile, the Python script invoked by CMake or pretty much any other scriptable build system. They are all untrusted scripts you download from the internet and run on your computer. Rust build.rs is not really special in that regard.

Maybe go build doesn't allow this but most other language ecosystems share the same weakness.

exDM69··on Myths About Floating-Point Numbers (2021)
It's not always necessary to check for inf/NaN explicitly using isinf/isnan. Both inf and NaN are floating point values with well defined semantics.

I'll give two examples from a recent project where I very intentionally divided by zero. First one was about solving a zero in a derivative and check if it falls on 0..1 range. This exploits the fact that (x < NaN) is always false and comparisons with +/- inf behave as expected.

    float t = a / b; // might divide by zero. NaN if a and b == 0.0, +/- inf if b == 0.0
    if (t > 0.0 && t < 1.0) {
        // we don't get here if t is +/- inf or nan
        split_at(t); // do the thing
    }
The second one was similar, but clamping to 0..1 range using branchless simd min/max.

    f32x8 x = a / b; // might divide by zero
    return simd_min(simd_max(0.0, x), 1.0); // returns 0 for -inf or nan, 1 for +inf
In both of these cases, explicitly checking for division by zero or isinf/isnan would've been (worse than) useless because just using the inf/NaN values gave the correct answer for what comes next.
exDM69··on Myths About Floating-Point Numbers (2021)
My favorite thing about floating point numbers: you can divide by zero. The result of x/0.0 is +/- inf (or NaN if x is zero). There's a helpful table in "weird floats" [0] that covers all the cases for division and a bunch of other arithmetic instructions.

This is especially useful when writing branchless or SIMD code. Adding a branch for checking against zero can have bad performance implications and it isn't even necessary in many cases.

Especially in graphics code I often see a zero check before a division and a fallback for the zero case. This often practically means "wait until numerical precision artifacts arise and then do something else". Often you could just choose the better of the two options you have instead of checking for zero.

Case in point: choosing the axes of your shadow map projection matrix. You have two options (world x axis or z axis), choose the better one (larger angle with viewing direction). Don't wait until the division goes to inf and then fall back to the other.

[0] https://www.cs.uaf.edu/2011/fall/cs301/lecture/11_09_weird_f...

exDM69··on Writing a Rust GPU kernel driver: a brief introduction on how GPU drivers work
> waiting required is of the form "X can't proceed until A, B, D, and Q are done

In my experience, this is the common pattern on GPU workloads. On the CPU (where async happens), the wait pattern is usually much simpler.

Of course there would need to be a way to not await on the CPU and pass "futures" as semaphore waits to GPU.

But all of this is just wild ideas at this point.

exDM69··on A fast, growable array with stable pointers in C
> Also, from a strictly prose point of view, isn't it strange that the `clz` instruction

It's using the `bsr` instruction which is similar (but worse). The `lzcnt` instruction in x86_64 is a part of the BMI feature introduced in Intel Haswell. The compiler does not generate these instructions by default so it runs on any x86_64.

If you add `-mbmi` or `-march=haswell` or newer to the compiler command line, you should get `clz`/`lzcnt` instead.

exDM69··on Writing a Rust GPU kernel driver: a brief introduction on how GPU drivers work
> Lots of Vulkan map quite well to Rust's ownership rules, the memory allocation API surface maps very well. But anything that's happening on the GPU timeline is pretty much impossible to do safely.

I agree with this, having been dabbling with Vulkan and Rust for a few years now. Destructors and ownership can make a pretty ergonomic interface to the cpu side of gpu programming. It's "safe" as long as you don't screw up your gpu synchronization which is not perfect but it's an improvement over "raw" graphic api calls (with little to no overhead).

As for the GPU timeline, I've been experimenting with timeline semaphores. E.g. all the images (and image views) in descriptor set D must be live as long as semaphore S has value less than X. This coupled with some kind of deletion queue could accurately track lifetimes of resources on the GPU timeline.

On the other hand, basic applications and "small world" game engines have a simpler way out. Most resources have a pre-defined lifetime, either it lives as long as the application, or the "loaded level" or the current frame. You might even use Rust lifetimes to track this (but I don't). This model is not applicable when streaming textures and geometry in and out of the GPU.

What I would really like to experiment with is using async Rust for GPU programming. Instead of using `epoll/kqueue/WaitForMultipleObjects` in the async runtime for switching between "green threads" the runtime could do `vkWaitForSemaphores(VK_SEMAPHORE_WAIT_ANY_BIT)` (sadly this function does not return which semaphore(s) were signaled). Each green thread would need its own semaphore, command pools, etc.

Unfortunately this would be a 6-12 month research project and I don't have that much free time at hand. It would also be quite an alien model for most graphics programmers so I don't think it would catch on. But it would be a fun research experiment to try.

exDM69··on QUIC for the kernel
The article that GP posted was specifically about throughput over a high speed connection inside a data center.

It was not about latency.

In my opinion, the lessons that one can draw from this article should not be applied for use cases that are not about maximum throughput inside a data center.

exDM69··on Rust running on every GPU
No, it does not. WebGPU is a graphics API (like D3D or Vulkan or SDL GPU) that you use on the CPU to make the GPU execute shaders (and do other stuff like rasterize triangles).

Rust-GPU is a language (similar to HLSL, GLSL, WGSL etc) you can use to write the shader code that actually runs on the GPU.

exDM69··on NIH is cheaper than the wrong dependency
Yes it can and yes indeed it's sometimes a pain in the ass.

However, some (maybe even most) C libraries' Rust bindings deal with this automatically when installed via Cargo.

exDM69··on Drones Are Key to Winning Wars Now. The U.S. Makes Hardly Any
Long distance communication with submarines is difficult but not impossible, there are four extra low frequency transmitters in the world to send signals in nuclear doomsday scenarios.

For shorter distances, there are acoustic and optical devices, and near surface some low frequency radios can be used.

https://en.m.wikipedia.org/wiki/Communication_with_submarine...

exDM69··on The messy reality of SIMD (vector) functions
It's actually Rust that is setting the 64 wide limit (see SupportedLaneCount), not LLVM.

I agree, f64x64 is probably a very bad idea.

But something like f32x8 would probably still be "fast enough" on old/mobile CPUs without 256 wide vectors (but good 128 bit SIMD ALU).

I did something like this when using a u16x16 bitmask fit the problem domain. Most of my target CPUs have 256 wide registers but on mobile ARM land they don't. This wasn't particularly performance sensitive code so I just used 256 bit wide vectors anyway. It wasn't worth it trying to optimize for the old CPUs separately.

exDM69··on The messy reality of SIMD (vector) functions
Your experience matches mine, you can get a lot done with the portable SIMD in Clang/GCC/Rust but you can't avoid the platform specific stuff when you need specialized instructions.

Depends on the domain you work in how much you need to resort to platform specific intrinsics. For me dabbling in computer graphics and game physics, almost all of the code is portable except for some rare specialized instructions here and there.

For someone working in specialized domains (like video codecs) or hardware (HPC super computers) the balance might be the other way around.

exDM69··on The messy reality of SIMD (vector) functions
> Practically, it really is hard, because SIMD instruction sets in CPUs are a mess. X86 and ARM have completely different sets of things that they have instructions for

Not disagreeing it's a mess, but there's also quite a big common subset containing all the basic arithmetic ops and some specialized ones rsqrt, rcp, dot product, etc.

These should be easier to use without having to write the code for each instruction set. And they are with C vector extensions or Rust std::simd.

Some of the inconsistencies you mention are less of a problem in portable simd, taking Rust for example:

- lzcnt and tzcnt: std::simd::SimdInt has both leading_zeros and trailing_zeros (also leading/trailing_ones) for every integer size and vector width.

- horizontal adds: notably missing from std::simd (gotta use intrinsics if you want it), but there is reduce_sum (although it compiles to add and swizzle). Curiously LLVM does not compile `x + simd_swizzle!(x, [1, 0, 3, 2])` into haddps

- absolute values for iBxN and fBxN out of the box.

Also these have fallback code (which is mostly reasonable, but not always) when your target CPU doesn't have the instruction. You'll need to enable the features you want at compile time (-C target-features=+avx2).

> With this kind of inconsistency, any portable SIMD abstraction will be difficult to efficiently compile for the majority of CPUs, negating part of the advantage.

I agree it negates a part of the advantage. But only a part, and for that you have zero cost fallback to intrinsics. And in my projects that part has been tiny compared to the overall amount of SIMD code I've written.

For basic arithmetic ops it's a huge win to have to write the code only once, and use normal math operations (+, -, *, /) instead of memorizing the per-CPU intrinsics for two (or more) CPU vendors.

exDM69··on The messy reality of SIMD (vector) functions
> if you don't describe your code and dataflow in a way that caters to the shape of the SIMD

But when I do describe code, dataflow and memory layout in a SIMD friendly way it's pretty much the same for x86_64 and ARM.

Then I can just use `a + b` and `f32x4` (or its C equivalent) instead of `_mm_add_ps` and `_mm128` (x86_64) or `vaddq_f32` and `float32x4_t` (ARM).

Portable SIMD means I don't need to write this code twice and memorize arcane runes for basic arithmetic operations.

For more specialized stuff you have intrinsics.

exDM69··on The messy reality of SIMD (vector) functions
Here's a toy example of f32 vs. f64 generic Rust std::simd code without macros. It aint't pretty but it works.

https://play.rust-lang.org/?version=nightly&mode=debug&editi...

In my projects, I've put a lot of related SIMD math code in a trait. It saves duplicating the monstrous `where` clause in every function declaration. Additionally it allows me to specialize so that `recip_fast` on f32 uses `__mm_rcp_ps` on x86 or `vrecpeq_f32`/`vrecpsq_f32` on ARM (fast reciprocal intrinsic function) but for f64 it's just `x.recip()` (which uses a division). If compiled on other than ARM or x86, it'll also fall back to `x.recip()` for portability (I haven't actually tested this).

The ergonomics here could be better but at least it compiles to exactly the assembly instructions I want it to and using this code isn't ugly at all. Just `x.recip_fast()` instead of `x.recip()`.

exDM69··on The messy reality of SIMD (vector) functions
> f64x64 is presumably an exaggeration

It's not. IIRC, 64 elements wide vectors are the widest that LLVM (or Rust, not sure) can work with. It will happily compile code that uses wider vectors than the target CPU has and split accordingly.

That doesn't necessarily make it a good idea.

Autovectorization works great for simple stuff and has improved a lot in the past decade (e.g. SIMD gather loads).

It doesn't work great for things like converting a matrix to quaternion (or vice versa), and then doing that in a loop. But if you write the inner primitive ops with SIMD you get all the usual compiler optimizations in the outer loop.

You should not unroll the outer loop like in the Quake 3 days. The compiler knows better how many times it should be unrolled.

I chose this example because I recently ported the Quake 3 quaternion math routines to Rust for a hobby project and un-unrolled the loops. It was a lot faster than the unrolled original (thanks to LLVM, same would apply to Clang).

exDM69··on The messy reality of SIMD (vector) functions
Here we are discussing the merits of built in SIMD facilities of not one but two programming languages. Waiting for the Zig guys to chime in to make it a three.

Four if you include LLVM IR (I don't).

No reason to be dismissive about it.

exDM69··on The messy reality of SIMD (vector) functions
The binary portability issue is not specific to Rust or std::simd. You would have to solve the same problems even if you use intrinsics or C++. If you use 512 but vectors in the code, you will need to check if the CPU supports it or add multiversioning dispatch or you will get a SIGILL.

I have written both type generic (f32 vs f64) and width generic (f32x4 vs f32x8) SIMD code with Rust std::simd.

And I agree it's not very pretty. I had to resort to having a giant where clause for the generic functions, explicitly enumerating the required std::ops traits. C++ templates don't have this particular issue, and I've used those for the same purpose too.

But even though the implementation of the generic functions is quite ugly indeed, using the functions once implemented is not ugly at all. It's just the "primitive" code that is hairy.

I think this was a huge missed opportunity in the core language, there should've been a core SIMD type with special type checking rules (when unifying) for this.

However, I still think std::simd is miles better than intrinsics for 98% of the SIMD code I write.

The other 1% (times two for two instruction sets) is just as bad as it is in any other language with intrinsics.

The native vector width and target-feature multiversioning dispatch are quite hairy. Adding some dynamic dispatch in the middle of your hot loops can also have disastrous performance implications because they tend to kill other optimizations and make the cpu do indirect jumps.

Have you tried just using the widest possible vector size? e.g. f64x64 or something like it. The compiler can split these to the native vector width of the compiler target. This happens at compile time so it is not suitable if you want to run your code on CPUs with different native SIMD widths. I don't have this problem with the hardware I am targeting.

Rust std::simd docs aren't great and there have been some breaking changes in the few years I've used it. There is certainly more work on that front. But it would be great if at least the basic stuff would get stabilized soon.

exDM69··on The messy reality of SIMD (vector) functions
This right here illustrates why I think there should be better first class SIMD in languages and why intrinsics are limited.

When using GCC/clang SIMD extensions in C (or Rust nightly), the implementation of sin4f and sin8f are line by line equal, with the exception of types. You can work around this with templates/generics.

The sin function is entirely basic arithmetic operations, no fancy instructions are needed (at least for the "computer graphics quality" 32 bit sine function I am using).

Contrast this with intrinsics where the programmer needs to explicitly choose the mm128 or mm256 instruction even for trivial stuff like addition and other arithmetic.

Similarly, a 4x4 matrix multiplication function is the exact same code for 64 bit double and 32 bit float if you're using built in SIMD. A bit of generics and no duplication is needed. Where as intrinsics again needs two separate implementations.

I understand that there are cases where intrinsics are required, or can deliver better performance but both C/C++ and Rust have zero cost fallback to intrinsics. You can "convert" between f32x4 and mm128 at zero cost (no instructions emitted, just compiler type information).

I do use some intrinsics in my SIMD code this way (rsqrt, rcp, ...). The CPU specific code is just a few percent of the overall lines of code, and that's for Arm and x86 combined.

The killer feature is that my code will compile into x86_64/SSE and Aarch64/neon. And I can use wider vectors than the CPU actually supports, the compiler knows how to break it down to what the target CPU supports.

I'm hoping that Rust std::simd would get stabilized soon, I've used it for many years and it works great. And when it doesn't I have a zero cost fallback to intrinsics.

Some very respected people have the opinion that std::simd or its C equivalent suffer from a "least common denominator problem". I don't disagree with the issue but I don't think it really matters when we have a zero cost fallback available.

← PreviousPage 3 of 34Next →