Jai language primer

On news.ycombinator I've found an article about the Jai language being designed by Jonathan Blow:

The purpose of Jai is to write high performance video games, and it's not a safe language, so if you look at it with Rust eyes you will see a soup of strange dangerous things. Yet Rust is not meant just to write browsers and very low-level code, you should be able to use Rust to write games too (see Piston).

Below I list two Jai features that I think could be interesting for Rust too.


An interesting feature of Jai is SoA/AoS:

Modern processors and memory models are much faster when spatial locality is adhered to. This means that grouping together data that is modified at the same time is advantageous for performance. So changing a struct from an array of structures (AoS) style:

struct Entity {
    Vector3 position;
    Quaternion orientation;
    // ... many other members here
};
Entity all_entities[1024]; // An array of structures
for (int k = 0; k < 1024; k++)
    update_position(&all_entities[k].position);
for (int k = 0; k < 1024; k++)
    update_orientation(&all_entities[k].orientation);
to a structure of arrays (SoA) style:
struct Entity {
    Vector3 positions[1024];
    Quaternion orientations[1024];
    // ... many other members here
};
Entity all_entities; // A structure of arrays
for (int k = 0; k < 1024; k++)
    update_position(&all_entities.positions[k]);
for (int k = 0; k < 1024; k++)
    update_orientation(&all_entities.orientations[k]);

can improve performance a great deal because of fewer cache misses.

However, as programs get larger, it becomes much more difficult to reorganize the data. Testing whether a single, simple change has any effect on performance can take the developer a long time, because once the data structures must change, all of the code that acts on that data structure breaks. So Jai provides mechanisms for automatically transitioning between SoA and AoS without breaking the supporting code. For example:

Vector3 :: struct {
    x: float = 1;
    y: float = 4;
    z: float = 9;
}
v1 : [4] Vector3;     // Memory will contain: 1 4 9 1 4 9 1 4 9 1 4 9
Vector3SOA :: struct SOA {
    x: float = 1;
    y: float = 4;
    z: float = 9;
}
v2 : [4] Vector3SOA;  // Memory will contain: 1 1 1 1 4 4 4 4 9 9 9 9

Getting back to our previous example, in Jai:

Entity :: struct SOA {
    position : Vector3;
    orientation : Quaternion
    // .. many other members here
}
all_entities : [4] Entity;
for k : 0..all_entities.count-1
    update_position(&all_entities[k].position);
for k : 0..all_entities.count-1
    update_orientation(&all_entities[k].orientation);

Now the only thing that needs to be changed to convert between SoA and AoS is to insert or remove the SOA keyword at the struct definition site, and Jai will work behind the scenes to make everything else work as expected.

Normal AoS Rust struct:

struct Foo {
    a: f32,
    b: f32,
    c: f32,
    d: f32,    
}

A SoA struct:

#[SOA]
struct Foo {
    a: f32,
    b: f32,
    c: f32,
    d: f32,    
}

This could be an intermediate point:

struct Foo {
    #[hot] a: f32,
    b: f32,
    c: f32,
    d: f32,    
}

It's useful when the 'a' field is hot while the others are cold, so you want the 'a' fields in a memory zone separated from the other fields, to increase cache coherence when you access the a fields often.

A related idea is to group fields:

struct Foo {
    #[SOA(1)] a: f32,
    #[SOA(1)] b: f32,
    #[SOA(2)] c: f32,
    #[SOA(2)] d: f32,    
}

It's useful if your code uses the "a" and "b" fields together often, and the "c" and "d" together often. But the other combinations of accesses (like a and c) are uncommonly done.


Jai allows inlining annotations at the function definition and also at the calling point:

test_a :: () { /* ... */ }
test_b :: () inline { /* ... */ }
test_c :: () no_inline { /* ... */ }

test_a(); // Compiler decides whether to inline this
test_b(); // Always inlined due to "inline" above
test_c(); // Never inlined due to "no_inline" above

inline test_a(); // Always inlined
inline test_b(); // Always inlined
inline test_c(); // Always inlined

no_inline test_a(); // Never inlined
no_inline test_b(); // Never inlined
no_inline test_c(); // Never inlined

Allowing #[inline]/#[noinline] at the calling point in Rust code sounds nice (I guess the annotation at the calling point overrides the attribute at the function definition point).

Currently the syntax is allowed:

#![feature(stmt_expr_attributes)]

#[inline(never)] pub fn foo(x: u32) -> u32 { x * 2 }

fn main() {
    use std::env::args;

    let x = args().nth(1).unwrap().parse::<u32>().unwrap();

    #[inline(always)] let y = foo(x);

    println!("{}", y);
}

But apparently the inline(always) is ignored, or it loses against the inline(never):

...
    je  .LBB1_32
    cmpq    %r15, %r14
    je  .LBB1_32
    movl    $1, %r8d
    movq    %rdi, %rcx
    movq    %r14, %rdx
    callq   __rust_deallocate
.LBB1_32:
    movzbl  64(%rsp), %eax
    cmpl    $212, %eax
    jne .LBB1_34
.Ltmp16:
    leaq    40(%rsp), %rcx
    callq   _ZN3sys2os9Args.Drop4drop20hfcd1409b1c697392sDxE
.Ltmp17:
.LBB1_34:
    shrq    $32, %rbx
    movl    %ebx, %ecx
    callq   _ZN3foo20h6a4eb31decab0346eaaE
    movl    %eax, 92(%rsp)
    leaq    _ZN3fmt3num16u32.fmt..Display3fmt20hbb2431870effd2eaxLVE(%rip), %rax
    movq    %rax, 104(%rsp)
    leaq    92(%rsp), %rax
    movq    %rax, 96(%rsp)
    leaq    ref4347(%rip), %rax
    movq    %rax, 40(%rsp)
    movq    $2, 48(%rsp)
    xorps   %xmm0, %xmm0
    movups  %xmm0, 56(%rsp)
...
4 Likes

Jonathan Blow’s opinions have always been a mystery. As far as I remember all his criticism about Rust have always been either very vague or misinformed.

In my opinion what’s missing the most right now for Rust to be considered as interesting for game development is a bunch of good libraries and game samples. Plus maybe a PR person that has experience in gamedev.

The core language only has one major flaw (that old gist which I already talked about), and the tools are getting there with the new multirust automatically installing the android SDK for example.

3 Likes

I think he doesn't want max safety, he wants something different.

IIRC, what he wants is implementation speed. Rust is just fundamentally incompatible with what Jonathan Blow seems to want in a language.

The way I’ve looked at it for a while is that Rust is not about writing code quickly: it’s about writing correct code quickly. It’s the tension between working out in an hour that your broken and unsafe code isn’t really what you want and throwing it away, versus working out in four days that the correct and safe code isn’t really what you want and throwing it away.

I’m currently playing with some Win32 GUI code. After two days, I’m nearly at the point where I can display text. Almost. The overwhelming majority of the time has been spent ever-so-carefully wrapping types and functions, pushing unsafety out.

If I was writing it in C++, I’d have my theoretical toolkit half implemented by now.

2 Likes

That's flawed because writing Rust bindings is similar to writing a C++ header. What you should compare is the time it takes to write a GUI after you have written the Rust bindings vs the time it takes in C++.

From what I've seen in the gist, if you compare what Blow wants to Rust:

  • Greater productivity than C/C++: check ; as far as I know for the moment everyone who has experience in both Rust and C++ have said that they were more productive in Rust.
  • Fast or instantaneous compilation: nope.
  • A language that allows you to write fast code: check.
  • Pragmatism and not idealism in the language's syntax: check.
  • Having direct access to the sharp tools: check, although a bit arguable.
  • Direct access to hardware/low-level programming: arguable, but mostly true.

I don't think he wants implementation speed. He just wants a low-level language that's better than C.

4 Likes

I think this is an amazing proposal. I hope people don't lose track of this idea. It's certainly useful to look at other language ideas and think about incorporating some of them in rust, as far as this makes sense. This could be a step towards more game dev compatibility in general, but there will be more problems people will complain about when actually using it for game dev.


I can only disagree. I've written a whole Gtk+ app in rust with the gtk-rs bindings. It took me a LOT more time than equivalent C code, because:

  • even if you manage to come up with rust bindings to a C/C++ framework, it will be very likely harder to use, because those don't map properly to idiomatic rust (example: static lifetime of gtk callbacks in gtk-rs, instead of widget-lifetime)... I doubt Jai will have these problems, since it doesn't introduce such "intrusive" concepts as rust does
  • you have to use indeed a whole "stack" of smart pointers, boxes and so on to be able to "talk" to the framework. In gtk-rs, you really cannot get away without a lot of RefCell, because of various lifetime issues. Jai will not have those problems either.
  • most likely, the bindings will be incomplete in the end, because some things cannot properly be mapped... and so you will have to come up with very fishy unsafe code at times, to do something you need to do. And then the rust compiler helps you even less than any C compiler, because it says "doh, unsafe code, deal with it"

Those are all problems, Jonathan probably does not want to deal with. C interoperability is a very important aspect of Jai.

Oh no, he wants implementation speed. That's what he says in pretty much all of his talks. He regularly starts with examples on how slow photoshop loads assets/pictures etc, how slow the GUI is and so on.

2 Likes

Just a quick note, this is actually runtime speed, not implementation speed. Implementation speed would be how long the (working) code takes to write.

That depends on which side of Photoshop you're on, it's "runtime speed" if you are developing Photoshop but "implementation speed" if you are using it. I haven't seen any of Jonathon Blow's talks, but given that he's a game developer and from a quick skim of the "The Philosophy of Jai" section in the linked document, I could very easily see him arguing that because Photoshop is so slow to work with the artists using it have an artificially lowered implementation speed. If they were given better, faster tooling then they could more quickly "implement" the artwork they are designing.

1 Like

In the conversation in this thread that the quote about implementation speed was taken from, implementation speed clearly referred to programmer productivity.

1 Like

I really like the #[SoA] idea. I’m often wondering whether to use AoS or SoA. AoS is often easier to use but SoA is arguably faster in some circumstances. However with the attribute-like macro you could have both at the same time. The macro would generate a SoA<MyStruct> type (not generic, but I guess you get what I mean) and automatic conversions between those types, e.g., SoA<MyStruct>::from(vec![s1, s2, s3]), soa.push(s), soa.view_struct(3), soa.fieldA[3]

I think I’ll give it some time on the weekend. This should be rather easy with a #[derive(SoA)] on stable. Implementing different memory areas without compiler support could be tricky though.

1 Like

I remember seeing at least one person implement something like this almost immediately after Rust 1.15 shipped and made custom derives stable. https://github.com/lumol-org/soa-derive is the first implementation I can find right now. Though I have no idea how complete/robust this is.

3 Likes

I’ve heard lately that the real speed up is to go from Structure of Arrays all the way to Array of Structure of Arrays, where you have an array of structs, and each struct holds an array of 8 things (or however many) so that when you work with two things that are far away from each other in the overall system they don’t bring in too much of the others and thrash your cache as much.

But i’m sure the flag can handle that eventual detail later on.

2 Likes

Source?

I heard about it in this talk by Edward Kmett, he kinda threw it out there without a specific paper link or anything, but you can watch the video:

(video description is in german but the talk is all in english)

1 Like

[Amethyst] solves this problem (SoA) using [specs], where each entities’ properties are stored in separate components, each with their own (usually array backed) storage.

I’m on my phone so can’t fill in the links sorry (use google).

Some more detail would be nice. What is Amethyst? Duckduck go (sorry no Google!) isn't finding much

It is a game engine written in rust.

1 Like

Thanks

Blow's comments on Rust were briefly touched on in the epic AMA with kyrenn from Chucklefish games (Starbound, Stardew Valley) who are making their next game with Rust.

https://www.reddit.com/r/rust/comments/78bowa/hey_this_is_kyren_from_chucklefish_we_make_and/dot1oo7/

Was Jon Blow correct in saying that enforcing memory safety is a wash for games development?

It's Jonathan Blow, so I'm inclined to say that you should take whatever he says and weigh that much higher than whatever I say. That being said, I really really really disagree.

The problem is not that you can write a bug, and then you run your game and your game crashes. The problem is not that you can write a bug, and then if you give your game ridiculous input it can crash. The problem is that you can write a bug, and 99.999% of the time your game works fine, and .001% of the time your game does something wrong and you don't know why. The problem is that your game works fine under error condition A, unless some other error B happens, and then your players lose their save because errors A and B are both safe in isolation, but if A happens and then B happens, that causes UB.

I remember at least 5 major instances where there were multiple programmers in the office, pouring over some minutia in the C++ standardese, trying to figure out if some corner of a corner case of C++ was UB or not. Can anybody really keep the default / zero / value initialization rules in their head? We thought we knew C++14 backwards and forwards and still managed to have uninitialized value bugs, and they're awful because depending on the situation they will mostly work. I can count three or four times in Starbound's history where I was bitten by just the std::unordered_map iterator invalidation rules, that one's awful because you have to trigger a rehash often times in order to trigger the bug. I hear people say things to the effect that C++17 "more or less" solves the safety issues, "just use modern C++!", but I promise we were way ahead on that front and that was not our experience at all. We basically used C++17 before there was a C++17 (we had our own Maybe, Variant, Either etc), and UB was still always lurking right there ready to bite. It was HARD to get right.

I didn't really mean for this to turn into a C++ bash rant, and I really don't want to be militant about it. It's possible that you don't have these problems in C++, maybe you're a better programmer than me? Seriously, it's possible I'm just not good enough. Certainly maybe Jonathan Blow is better than me!

13 Likes

For me, the most interesting ideas from the JAI primer are full runtime type introspection (which is amazing for writing an engine) and arbitrary compile time execution. Those two are not really fulfilled by rust besides compile speed. Rust macros are cool, and custom derive can do a lot of magic, but it would be even cooler to just write rust as a build script/data preprocessing step/whatever and have the compiler create LUTs by using the same code as at runtime.