←back to thread

53 points mvx64 | 4 comments | | HN request time: 0.001s | source

So, after years of abandoning Rust after the hello world stage, I finally decided to do something substantial. It started with simple line rendering, but I liked how it was progressing so I figured I could make a reasonably complete PSX style renderer and a game with it.

My only dependency is SDL2; I treat it as my "platform", so it handles windowing, input and audio. This means my Cargo.toml is as simple as:

[dependencies.sdl2] version = "0.35" default-features = false features = ["mixer"]

this pulls around 6-7 other dependencies.

I am doing actual true color 3D rendering (with Z buffer, transforming, lighting and rasterizing each triangle and so on, no special techniques or raycasting), the framebuffer is 320x180 (widescreen 320x240). SDL handles the hardware-accelerated final scaling to the display resolution (if available, for example in VMs it's sometimes not so it's pure software). I do my own physics, quaternion/matrix/vector math, TGA and OBJ loading.

Performance: I have not spent a lot of time on this really, but I am kind of satisfied: FPS ranges from [200-500] on a 2011 i5 Thinkpad to [70-80] on a 2005 Pentium laptop (this could barely run rustc...I had to jump through some hoops to make it work on 32 bit Linux), to [40-50] on a RaspberryPi 3B+. I don't have more modern hardware to test.

All of this is single threaded, no SIMD, no inline asm. Also, implementing interlaced rendering provided a +50% perf boost (and a nice effect).

The Pentium laptop has an ATI (yes) chip which is, maybe not surprisingly, supported perfectly by SDL.

Regarding Rust: I've barely touched the language. I am using it more as a "C with vec!s, borrow checker, pattern matching, error propagation, and traits". I love the syntax of the subset that I use; it's crystal clear, readable, ergonomic. Things like matches/ifs returning values are extremely useful for concise and productive code. However, pro/idiomatic code that I see around, looks unreadable to me. I've written all of the code from scratch on my own terms, so this was not a problem, but still... In any case, the ecosystem and tooling are amazing. All in all, an amazing development experience. I am a bit afraid to switch back to C++ for my next project.

Also, rustup/cargo made things a walk in the park while creating a deployment script that automates the whole process: after a commit, it scans source files for used assets and packages only those, copies dependencies (DLLs for Win), sets up build dependencies depending on the target, builds all 3 targets (Win10_64, Linux32, Linux64), bundles everything into separate zips and uploads them to my local server. I am doing this from a 64bit Lubuntu 18.04 virtual machine.

You can try the game and read all info about it on the linked itch.io page: https://totenarctanz.itch.io/a-scavenging-trip

All assets (audio/images/fonts) where also made by me for this project (you could guess from the low quality).

Development tools: Geany (on Linux), notepad++ (on Windows), both vanilla with no plugins, Blender, Gimp, REAPER.

Show context
armitage__ ◴[] No.45271913[source]
I love custom game engines. Looks fantastic! Will you be sharing the source?
replies(1): >>45272932 #
mvx64 ◴[] No.45272932[source]
Thanks! For several reasons, most probably and regrettably no, for now at least.

More than happy to talk about any specific part however (e.g. how scenes are handled, the code itself, or how particular features are implemented or optimized).

replies(2): >>45273922 #>>45276406 #
galangalalgol ◴[] No.45273922[source]
Could you talk more about the subset of rust you settled on?

You said you didn't explicitly use simd, but did you do anything to help the optimizer autovectorize like float chunking

replies(1): >>45274054 #
mvx64 ◴[] No.45274054[source]
It's a very procedural style. I have not used: iterators, lifetimes, Arcs/Boxes/RefCells and whatnot, any kind of generics, data structures other than vecs/arrays, async, and many more. Also avoided functional style, builder patterns...

I only used traits to more easily implement the scenes; a Scene needs to implement a new(), a start() and an update(), so that I can put them in an array and call them like scenes[current_scene_idx].update() from the main loop.

Also, I used some short and simple closures to avoid repeating the same code in many places (like a scope-local write() closure for the menus that wraps drawtext() with some default parameters).

The vast majority of the time is spent in the triangle filling code, where probably some autovectorization is going on when mixing colors. I tried some SIMD there on x86 and didn't see visible improvements.

Apart from obvious and low-hanging fruit (keeping structs simple, keeping the cache happy, don't pass data around needlessly) I didn't do anything interesting. And TBH profiling it shows a lot of cache misses, but I didn't bother further.

replies(2): >>45274674 #>>45276421 #
galangalalgol ◴[] No.45274674[source]
Without lifetimes arcs boxes or refcells do you have a lot of clones? Or a lot of unsafe? Or is it mostly single threaded?
replies(1): >>45274969 #
1. mvx64 ◴[] No.45274969[source]
It's all single threaded. Just structs being passed around. For example, the mesh drawing call is:

drawmeshindexed(m: &Mesh, mat: &Mat4x4, tex: &Image, uv_off: &TexCoord, li: &LightingInfo, cam: &Camera, buf: &mut Framebuffer)

so there is also no global state/objects. All state is passed down into the functions.

There were some cases that RefCells came in handy (like having an array of references of all models in the scene) and lifetimes were suggested by the compiler at some other similar functions, by I ended up not using that specific code. To be clear, I have nothing against those (on the contrary), it just happened that I didn't need them.

One small exception: I have a Vec of Boxes for the scenes, as SceneCommon is an interface and you can't just have an array of it, obviously.

replies(1): >>45275466 #
2. galangalalgol ◴[] No.45275466[source]
Thanks! That seems like a nice subset for a lot of use cases. You say it isn't functional, which in rust it is hard to be pure, but if you consider it a spectrum, the style you describe is closer than most game code I've seen.
replies(1): >>45275866 #
3. mvx64 ◴[] No.45275866[source]
Right, it's a spectrum, you can't avoid some things (and rightly so).

Another soft rule: no member functions (except for the Scenes); structs are only data, all functions are free functions.

Also no operator overloading, so yes, lots of Vec3::add(&v1, &v2). I was hesitant at first but this makes for more transparent ops (* is dot or cross?) and does not hide the complexity.

The whole thing is around 6-7kloc and I think it would be possible to rewrite in C++ in a day or two.

replies(1): >>45277611 #
4. galangalalgol ◴[] No.45277611{3}[source]
I don't find the idea of rewriting the subset of a blas library I want to use for each project very fun, but I bet with so few dependencies a stripped binary gets pretty small.