Hacker News new | past | comments | ask | show | jobs | submit login
Mach v0.1: Cross-platform Zig graphics (hexops.com)
190 points by andreafeletto on March 27, 2022 | hide | past | favorite | 47 comments



Good article, 1 comment, still.

> slugging through the depths of hell where only a footnote from Raymond Chen himself will save you

The behavior is well documented: https://docs.microsoft.com/en-us/cpp/build/x64-calling-conve...

the caller must allocate memory for the return value and pass a pointer to it as the first argument. The remaining arguments are then shifted one argument to the right. The same pointer must be returned by the callee in RAX


This is also fairly standard as a calling convention as well? The Itanium C++ ABI (the standard for x64 on Linux) has the same text: https://itanium-cxx-abi.github.io/cxx-abi/abi.html#non-trivi...

So either there's some extra COM consideration I'm not aware of, or there's a serious confusion in how calling conventions work. If someone returns an aggregate, what option do you have other than a pointer? If it's a callee-allocated pointer, what are the lifetime rules and how do you free it? Passing caller-allocated storage is the obvious answer and is far from unusual, or undocumented.

The difference I can tell between the MSVC and Itanium C++ ABIs is just that MSVC uses "this, ret" order for methods, and Itanium uses "ret, this". There's probably some subtle differences in what constitutes "non-trivial" between the two ABIs as well.


Apparently, the OP’s issue was ABI incompatibility between VC++ (also used by Windows SDK and everything built on top, including Direct3D 12), and System V (used on Linux, also MinGW on Windows).

VC++ documentation says structures are returned through a caller-allocated pointer. Pretty sure modern VC++ optimizes that thing when it can, especially with link time code generation enabled, but for functions and COM methods imported from DLLs that’s not an option.

System V http://c9x.me/compile/doc/abi.html only classifies values as MEMORY when they exceed 16 bytes. The D3D12_CPU_DESCRIPTOR_HANDLE only has a single size_t field, and therefore returned in RAX register in that convention. Moreover, a structure of two size_t fields would be returned in RAX + RDX registers.


System V is only a C ABI, not a C++ ABI, so of course it wouldn't cover C++-only details. What C++ ABI does MinGW use on Windows?


That thing is COM, which is a small subset of C++ ABI. Technically it’s about the same as on Windows, i.e. C ABI with extra first argument for this pointer.

Once upon a time I made this library https://github.com/const-me/comlightInterop/ The native side of the interop is idiomatic C++, here’s an example https://github.com/Const-me/ComLightInterop/blob/master/Demo... The C# side of the interop is implemented through the built-in C interop, here’s the relevant part of the library https://github.com/Const-me/ComLightInterop/blob/master/ComL... I’ve tested Linux version of that library on AMD64, ARMv7, and ARM64 CPUs, but only with gcc compiler on the native side.


My memory on this specific issue is a bit fuzzy, but as far as I remember the issue was regarding the definition/ABI compatibility of C++ member functions. The doc you linked aren't really clear what to do in the case of C++ member functions, as they always take a hidden "this" class instance pointer as the first argument (in MSVC). So which one is passed first when you have both?

If you look at godbolt/or disassembly output of an MSVC compiled binary, you can find that it is actually passing the class pointer first, then the return value pointer. Which definitely doesn't match the absolute "the caller must allocate memory for the return value and pass a pointer to it as the first argument" statement from their own docs.

Prior discussion about this at https://news.ycombinator.com/item?id=29613640


There is also an updated Raymond Chen footnote: https://devblogs.microsoft.com/oldnewthing/20220113-00/?p=10...


This is awesome! I was hoping someone would take WebGPU/Dawn and package it up in a form that is usable outside Chromium. Building it yourself is nontrivial.

Portable graphics has always been a big issue. Back in the day OpenGL was kinda sorta portable, but in practice that never worked out very well. Drivers were buggy, platforms came with different versions and different extensions, Windows didn't come with GL drivers by default, mobile had OpenGL ES which was different. And now Apple has deprecated OpenGL so the dream of portable graphics at the OS level is truly dead.

WebGL is the most portable graphics API that exists today, and it's actually possible to use it from native code if you want (using the ANGLE library) but I don't think anyone does, and that's understandable. WebGPU should be different. I think there are a lot of good reasons that native apps should consider using WebGPU.


wgpu-native is easy to use. It's written in Rust, but it exposes a C API, so it's simple to use it from other languages. And building it yourself is also simple because it's a single cargo build away.


Author here, thanks for posting!

Happy to answer any questions about Mach (or my experience working with Zig.)


> There are few things in life that I am more serious about than this. I dedicate ~48 hours/week to my dayjob, and ~50h/week to Zig building Mach and running zigmonthly.org. After three years of aggressively pushing for progress in this exact way, I have no plans to slow down anytime soon.

This is really awesome work, and if keeping this pace works for you all the better. But I just wanted to encourage you to keep any eye on that, and to make sure that you're not pushing yourself beyond what you're able or willing to put into it. I know many members of the open source community can struggle with that balance, and that many of those people would also rather you building stuff like this for 1/5n hours a week for the next 25 years than n hours a week for 5 years and burn out your interest or capacity. Thanks again for building this project, really cool to see!


Thanks :) I'm in this for the long haul and will keep this in mind.


Just wanted to say I love the style of your blog (especially the little diagrams), and am looking forward to future posts in the ECS series.

Also, I've played around with the mach-glfw-vulkan-example project (https://github.com/hexops/mach-glfw-vulkan-example) and really enjoyed using it to learn about Vulkan, so thank you for your part in that!

I think you make a good case for WebGPU, and am excited to look through the resources you shared – and the future of mach!


Hi Stephen,

The initial example works for me. Truly impressive packaging you've achieved.

I tried to build for musl, but it still links with glibc; is musl supported, or am I doing it wrong?

  motiejus ~/code/mach/gpu $ zig build   -Dtarget=x86_64-linux-musl
  motiejus ~/code/mach/gpu $ ldd zig-out/bin/gpu-hello-triangle
        linux-vdso.so.1 (0x00007fff573a3000)
        libX11.so.6 => /lib/x86_64-linux-gnu/libX11.so.6 (0x00007f188c836000)
        libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f188c6f2000)
        libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f188c6d0000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f188c50b000)
        libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f188c505000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f188c9a7000)
        libxcb.so.1 => /lib/x86_64-linux-gnu/libxcb.so.1 (0x00007f188c4da000)
        libXau.so.6 => /lib/x86_64-linux-gnu/libXau.so.6 (0x00007f188c4d3000)
        libXdmcp.so.6 => /lib/x86_64-linux-gnu/libXdmcp.so.6 (0x00007f188c2cd000)
        libbsd.so.0 => /lib/x86_64-linux-gnu/libbsd.so.0 (0x00007f188c2b6000)
        libmd.so.0 => /lib/x86_64-linux-gnu/libmd.so.0 (0x00007f188c2a9000)
Also, I wasn't able to cross-compile to aarch64, because it does not seem to recognize `-Ddawn-from-source=true`, and I wasn't able to figure out where to put the `Options.from_source = true`:

  motiejus ~/code/mach/gpu $ zig build -Ddawn-from-source=true    -Dtarget=aarch64-linux-musl
  error: gpu-dawn binaries for aarch64-linux.3.16...5.10.81-gnu.2.19 not available.
  error: -> open an issue: https://github.com/hexops/mach/issues
  error: -> build from source (takes 5-15 minutes):
  error:        use -Ddawn-from-source=true or set `Options.from_source = true`

  error: the following build command failed with exit code 1:
  /home/motiejus/code/mach/gpu/zig-cache/o/a1cea9683aaa7b7e7a533b8f6e40e10f/build /home/motiejus/code/zig-linux-x86_64-0.10.0-dev.896+e620b692c/zig /home/motiejus/code/mach/gpu /home/motiejus/code/mach/gpu/zig-cache /home/motiejus/.cache/zig -Ddawn-from-source=true -Dtarget=aarch64-linux-musl
  motiejus ~/code/mach/gpu $


(we chatted in the Matrix room, the TL;DR was there are some bugs here I need to fix and indeed aarch64-linux does not work yet :) https://matrix.to/#/!bIJeTLtOHucXQpqIcH:matrix.org/$FgXuXXmH... )


What do you think of Zig so far?

How do you recommend people get into it, especially those who don't have systems programming experience? Should you learn C first?


I'm super productive in Zig, I really like the language.

Documentation is lacking, so you're going to be reading the stdlib code. There are bugs. The current focus is on building the new stage2 compiler (going from C++ -> Zig implementation) so there are things that need attention that don't get it, yet.

I would not suggest anyone learn C first if they don't have systems programming experience. I'd go for Zig, Rust (or Go, if you just want to dip your toes into the land of pointers) and depending on how comfortable you feel. If you start with one and find it super frustrating, try another, the difference between them is huge. You might personally feel more productive in one, leading to you learning more.


Same here. As a compiled language, I find Zig super productive.

There's neither "magic" nor an accumulation of abstraction layers. As a result, when there's a bug in my code, I can quickly understand what's going on and how to fix it. This is very refreshing.


Thanks, this is helpful!

Zig appeals to me for its philosophy, simplicity, community and cross-platform build tools. The main reason I haven't already adopted it is the lack of docs/educational material that has made getting started pretty rough for a frontend dev. A lot of the existing docs seem to start with the assumption that you're a lifelong C hacker who is already comfortable with manual memory management, libc, allocators, linking and all the rest, and that you're coming to Zig to gain quality of life improvements over C.

I've dabbled with Rust/Go and found the docs/books much more accessible. But I didn't end up feeling an affinity for either language quite the same as Zig. (For Rust it was the complexity/readability and sluggish compile/dev loop; for Go it was the feeling that it wasn't really empowering me to do much that I couldn't already do quite comfortably with TypeScript/Deno, or stretching me to understand lower-level programming the way I'd hoped.)

Thank you for zigmonthly.org and for Mach! I just sponsored you on GitHub. It's encouraging to hear buzz around Zig and see it being used for fun things. I'm tempted to persevere with Zig and write docs/guides pitched at those outside the traditional systems programming realm as I gain confidence with it.


Super excited for Zig to make low-level coding more accessible to people outside the usual group of lifelong C hackers, as you said! That's one of the reasons I am betting on it.

Check out https://zig.news too, it's where a ton of people are writing learning material / little tutorials on things.


i'm following zig's project from HN and i often see cross-compilation and c/c++ mentionned.

Why is zig often mentionned in that context ? it seems that cross-compilation and C can be achieved using llvm/clang without a lot of problem ? What is zig accomplishing in that matter that makes it so special ?


Do you have experience cross compiling? If so, you’re likely familiar with the headache: getting the right tool chains, sysroots, compiler flags, libraries, etc. There are entire businesses and open source tools whose raison d'être is providing these tool chains and making the process simpler.

With Zig, that all goes away. It’s as simple as saying `zig build -Dtarget=aarch64-linux-gnu` or `zig build -Dtarget=riscv32-freestanding-none` and it just works. It’s pretty incredible.

Zig has set the bar for how cross compilation should be done and all other languages should be measured against it. Rust is close but is still a headache. The only other language that compares at the moment (IMO) is Go.


Huh. From what you said and then a quick peek at the docs Zig does seem great in that regard, but in my own experience I’ve definitely found Rust to be equally competent.

What about Zig would you say makes it better? Rust has `cargo build --target <triple>`


Cross compiling for the language itself is half of it, albeit a half Rust does decently well too, but Zig will also cross compile C/C++ for you just as easily. The latter part combined with Zig's focus on making it as 0 effort as possible to do either action is what results in it being talked about so much.


Admittedly I don’t have as much experience cross compiling with Rust, but my experience has not been so smooth. You need to install each target toolchain manually with rustup, and then some tool chains require different versions of Rust (e.g. the ESP32 RISC-V toolchain requires nightly). Another time I tried cross compiling on x86 Linux targeting Linux on ARM64, and I kept getting linking errors (or it couldn’t find an acceptable linker).

I certainly agree that Rust is much better than C/C++, but it’s not quite at Zig level, at least in my (limited) experience.


From what I've heard, for Rust the difficulty comes in when you're using a Rust wrapper of a C/C++ library and you need to crosscompile both into the binary.


I use Zig and cargo-zigbuild to cross-compile my Rust projects. It simply works, without Docker, if I want it to use older GLIBC symbols for an embedded Linux target running an ancient kernel/glibc version, or if I want to cross-compile to an M1 Mac from Linux. Of course, you still need to provide all the pkg-config/libraries/headers for the target platform, but that’s simple enough.


Best place to read more is probably on the Zig project website, specifically this section[1] on how/why Zig's compiler is also a C compiler and the benefits that provides.

[1] https://ziglang.org/learn/overview/#zig-is-also-a-c-compiler


So, I read that, and it is vague about whether the zig compiler itself compiles C, or whether it launches a C compiler (cc).

---

I will resist making the obvious reference despite having said both "launch" and "zig" in the same sentence.



The zig compiler itself (which is written in Zig) compiles C, `zig cc` just indicates to the zig compiler to parse C compiler options (`zig cc` has the same interface as Clang).


Here's the problem statement in a nutshell. Consider the following trivial C program:

    #include <stdio.h>
    #include <sys/random.h>
    int main(void) {
        unsigned char buf[10];
        ssize_t ret;
        ret = getrandom(buf, sizeof(buf), 0);
        if (ret != sizeof(buf))
            fprintf(stderr, "getrandom() failed\n");
        else
            fprintf(stderr, "getrandom() success\n");

        return ret != sizeof(buf);
    }
You have an x86_64 Linux machine running glibc 2.17. Your task is to compile the above program into a dynamically linked executable targeting glibc 2.28 on x86_64 Linux.

With Zig, it looks like this:

1) Download zig

2) `zig cc -target x86_64-linux-gnu.2.28 random.c`

What would those steps look like using llvm/clang?

Next, try doing it from Windows 8


It's simpler with clang.

  clang random.c
glibc 2.28 will work with binaries compiled against 2.17.


Unfortunately that won't compile against glibc 2.17 since getrandom(2) was only added to glibc in 2.25. See https://lwn.net/Articles/711013/ for background info,


nvm then


llvm/clang does not cross-compile "without a lot of problems". See this post from Zig's creator: https://andrewkelley.me/post/zig-cc-powerful-drop-in-replace...

zig cc uses clang under the hood, and Zig the language is also mostly based on LLVM, but zig cc ships a lot of versions of libc to ensure that it can properly compile to all platforms without weird linker errors.


TL;DR the hardest part of cross compilation is usually getting the tool gains set up to do so. Zig ships a bunch of libc's in a lightweight compressed format which makes cross compilation completely trivial, it handles most of the work behind the scenes.


How does it hande openssl ?


What do you mean? It can link against OpenSSL just like any other library. I don’t know if OpenSSL supports static builds, but if it does you can link it statically as well.


For cross compilation with dynamic linking I mean.

Openssl is typically one you want to be kept up to date with OS updates.

Like libc, it can be a pain.


What’s the value added in this over RayLib? Is it just building GLFW from source to dodge the dependency issues? Granted, that’s a lot bigger deal than I’m making it seem.


raylib is awesome and a great source of inspiration for me. It's definitely way more complete & comprehensive today than Mach is, too.

Longer term, I think you'll find raylib to be better for simpler applications, and Mach looking a little more like an Unreal/Unity/Godot & geared towards more complex applications. All speculation, though, they're both improving rather quickly!

The other obvious difference would be language it's written in (C vs. Zig)


> Mach looking a little more like an Unreal/Unity/Godot

Right now, Mach is probably something more like Bevy, Magnum.Graphics or BGFX. ie: a _framework_.

But your post alludes that that is not the goal. That Mach seeks to build asset importing, animation tools, navigation system, etc? ie: A FULL GAME ENGINE!?


I'm kind of new to game dev and I'm a little confused about the terminology you're using. You called Bevy a framework to differentiate it from an engine, but on the Bevy website they call themselves an engine. What would you say is differentiation between a framework and an engine?


While I've never seen it defined this way, I think the distinction comes down to this:

An engine deals with assets in depth, and exposes runtime abstractions to them.

A framework does what is necessary to expose I/O and load the assets, but aims to be minimally opinionated about how they integrate into the application.

For example, in an engine you might have abstractions for applying collision, physics, and rendering techniques onto an image asset, so that you can achieve a useful result just by configuring the data correctly. A framework would give you a way to load the image and display it, but leave it to you to add more detailed behaviors by writing new code.

In effect, engines are never totally done until you have a feature-complete application. Often it works out that you need a scripting layer to define a certain kind of asset(e.g. dialogue tree logic), and then what you have is an engine extension; if the asset is defined independently of the scripting system, it can be made portable to another engine.


I think the key difference in most people's eyes is: am I interacting with this mostly through code (framework), or is there a substantial amount of graphical tooling that aids me (engine)?

I'd say Bevy today is more akin to a framework, but they intend to add tooling etc. to become closer to an engine later on from what I understand.

framework <-> engine is more of a spectrum.


Yes. I don't think we need more frameworks, there are already great options there.

The interesting thing to work on in this space is tooling, so that's where we're headed: full game engine.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: