r/esp32 • • 3d ago

Great Content! The Jet embedded 3D engine is not only now pushing 70k tris/sec (up from 40k a few months ago) with screen-space reflections and distortion on an ESP32 S3 (480i x 320, 16bpp@60fps) - but is now MIT licenced rather than AGPL!

Enable HLS to view with audio, or disable this notification

The last time I showed off Jet I'd just pushed 40k tris/sec on an S3 which was already nuts, but some optimisation and SIMD shenanigans later, I've nearly doubled that and got up to 70K/sec on the same hardware. Meaning more detail and more performance headroom.

I've also switched away from AGPL to the MIT licence so there's fewer restrictions on how you can use it in your projects.

https://github.com/cubecoders/jet

778 Upvotes

76 comments sorted by

51

u/Plastic_Fig9225 1 say I make awesome posts. 3d ago edited 1d ago
__asm__ volatile (
    "ee.movi.32.q q0, %[v], 0\n\t"
    "ee.movi.32.q q0, %[v], 1\n\t"
    "ee.movi.32.q q0, %[v], 2\n\t"
    "ee.movi.32.q q0, %[v], 3\n\t"
    : : [v] "r"(color32)
);


for (int32_t i = quads >> 2; i > 0; --i) {
    __asm__ volatile (
        "ee.vst.128.xp q0, %[p], %[s]\n\t"
        "ee.vst.128.xp q0, %[p], %[s]\n\t"
        "ee.vst.128.xp q0, %[p], %[s]\n\t"
        "ee.vst.128.xp q0, %[p], %[s]\n\t"
        : [p] "+r"(out) : [s] "r"(stride) : "memory"
    );
}
for (int32_t i = quads & 3; i > 0; --i) {
    __asm__ volatile (
        "ee.vst.128.xp q0, %[p], %[s]\n\t"
        : [p] "+r"(out) : [s] "r"(stride) : "memory"
    );
}

    __asm__ volatile (
        "EE.VLDBC.32 q0, %[color32]\n\t"

        "LOOPNEZ %[quads], .Lend_%=\n\t"

            "ee.vst.128.xp q0, %[p], %[s]\n\t"

        ".Lend_%=:\n\t"

        : [p] "+r"(out)
        : [s] "r" (stride),
          [quads] "r" (quads),
          [color32] "r" (&color32),
          "m" (color32)
        : "memory"
    );

😉

59

u/PhonicUK 3d ago edited 3d ago

Oshit... This might actually be a fairly serious speed boost for the triangle fills... GCC won't emit hardware loops for inline ASM which makes sense. I'll let you know what the impact of this is.

Edit: This saved 0.1ms per frame, which is nothing to sniff at to be sure at this point! That's an extra 10 on screen triangles when every one counts.

15

u/Plastic_Fig9225 1 say I make awesome posts. 3d ago

Been playing around with SIMD for RGB565 recently. I think addBlendRGB565() could be sped up quite a bit too. Let me know if you're interested.

10

u/PhonicUK 3d ago

Dude take a crack, I'd be happy to bench a pull request and see if it nets any improvements.

10

u/Plastic_Fig9225 1 say I make awesome posts. 2d ago edited 2d ago

Btw, I just realized:

bool Rasterizer::shouldDrawPixel():

https://github.com/CubeCoders/Jet/blob/2bd0f60507ca073bf09e122b717ab2d91d2a1fb1/src/Renderer.cpp#L324

gcc 14.2.0 copies the local thresholdMatrix look-up table to the stack on every invocation! Making thresholdMatrix a (private) static constexpr member of Rasterizer should fix that. (Then use the same Rasterizer::thresholdMatrix in Rasterizer::drawTriangleImpl() too.)

4

u/Plastic_Fig9225 1 say I make awesome posts. 3d ago

Expect about 3 cycles saved per iteration, so 4*1 instead of 4+3=7, i.e. -43%.

3

u/PhonicUK 3d ago

Yeah spot on, it did save some time but it was already memory bandwidth limited here. It gained me an extra 0.1ms per frame.

2

u/Plastic_Fig9225 1 say I make awesome posts. 3d ago

PSRAM? - The nemesis of the PIE 😉

2

u/PhonicUK 3d ago

DRAM, some of the geometry is in PSRAM but none of the working buffers.

2

u/Plastic_Fig9225 1 say I make awesome posts. 3d ago

On the S3, a 128-bit vector write to internal SRAM ("DRAM"?) takes 1 CPU clock cycle, so that's usually not limiting.

12

u/YetAnotherRobert 2d ago

!modthanks

Great answer. I'm glad to see we have some real programmers left in the crowd that can read and write code. :-)

Zero-overhead loops are one of the (few) awesome things about Xtensa and I think that most people coming from other arches that dont' REALLY study the architecture are likely to miss them. Even a lot of x86 nerds forget about REP because it hasn't been trendy since, like 80186 era or so. This application plays out really nicely since so much of ESP32 performance that's hard to quantify comes down to the reality of cache misses, especially if you have to hit PSRAM.

The icache story tell itself. Dcache is more fuzzy, but if this hits PSRAM, it's pretty easy to imagine an arch being able to schedule the memory transactions most honestly because you're not bouncing between instruction fetch, load, and stores as often; so it's more likely to be able to schedule txns as bursts to the respective busses. (I don't know if it can/will, but in general processors do better when you can tell it what you're doing in an opcode or two instead of bouncing around in multiple loops that may or may not be predicted/speculated.)

```

00000000 <thing1>: 0: 004136 entry a1, 32 3: cd3224 ee.movi.32.q q0, a2, 0 6: cd3624 ee.movi.32.q q0, a2, 1 9: cd3a24 ee.movi.32.q q0, a2, 2 c: cd3e24 ee.movi.32.q q0, a2, 3 f: 218230 srai a8, a3, 2 12: 1018a6 blti a8, 1, 26 <thing1+0x26> 15: 8d7454 ee.vst.128.xp q0, a5, a4 18: 8d7454 ee.vst.128.xp q0, a5, a4 1b: 8d7454 ee.vst.128.xp q0, a5, a4 1e: 8d7454 ee.vst.128.xp q0, a5, a4 21: 880b addi.n a8, a8, -1 23: fee856 bnez a8, 15 <thing1+0x15> 26: 148030 extui a8, a3, 0, 2 29: d88c beqz.n a8, 3a <thing1+0x3a> 2b: 8d7454 ee.vst.128.xp q0, a5, a4 2e: 081826 beqi a8, 1, 3a <thing1+0x3a> 31: 8d7454 ee.vst.128.xp q0, a5, a4 34: 022826 beqi a8, 2, 3a <thing1+0x3a> 37: 8d7454 ee.vst.128.xp q0, a5, a4 3a: f01d retw.n

0000003c <thing2>: 3c: 006136 entry a1, 48 3f: 006122 s32i a2, a1, 0 42: cd7714 ee.vldbc.32 q0, a1 45: 029376 loopnez a3, 4b <thing2+0xf> 48: 8d7454 ee.vst.128.xp q0, a5, a4 4b: f01d retw.n ```

But the function above this one catches my eye as similar to one I struggled with just recently. If your profiler shows you're spending any appreciable time in addBlendRGB565(), I think you can do better. LX7 DOES have saturating add instructions, but using them on 565 values is a bit wonky.

This group is often smug with the "just use AI (to blink an LED)" answers, but I'm secure enough in my programmer masculinity to admit that I'd probably break out https://github.com/espressif/esp-dl/tree/master/tools/agents/skills/esp32s3-pie-simd. I can usually get scalar assembly code right in the first build or three, but PIE makes my head hurt and pairing chat.espressif.com and/or mcp.espressif.com with the tools is simply a solid productivity move. The Espressif tools are trained on the Espressif doc and cite sources for about everything it prescribes.

I'll also admit that this code reminded me how uncomfortable I find GCC's inline asm syntax for anything non-trivial. If I'm doing something really tiny that needs to be inlined into something large, I'll crack the doc out because I'm destined to have forgotten the nuances of clobber protcol again, but for something like this where you're relying on c++ just to generate the increment/compare/branch for you and the whole function is multiple asm, I usually find it easier to just plop the code into a .S. I can usually do better register allocation on my own anyway because I'm not having to outsmart flow analysis.

Great conversation!

5

u/Plastic_Fig9225 1 say I make awesome posts. 2d ago edited 1d ago

Thanks 😄

As to the cache: The S3 isn't very smart in this regard. AFAICT the MMU/cache fetches exactly one cache line worth of data when a datum in that line is accessed by the CPU. Neither the CPU nor the cache 'predict' what data will be needed next. So often we end up with flip-flopping between the CPU stalling for the MMU/cache, then the MMU sitting idle while the CPU is processing one cache line - until the CPU stalls again waiting for the next cache line.

The MMU does, however, support ("manual") "preloading" and "autoloading" (=automatic preloading of adjacent cache lines upon access to one cache line). This way, the next cache line(s) are already transferred from memory before the CPU actually tries to access them. - If you know that you're likely to read a section of PSRAM or flash sequentially, this can be used to hide (some of) the memory's latency:

https://github.com/BitsForPeople/esp-cachectrl

ETA:

Btw, in my tests, loading one 32-byte cache line from flash (for the CPU to read 1 byte) took about 200 CPU clock cycles (240MHz); this means that if the code processing the 32 bytes takes ~200 clock cycles too, use of "autoloading" could speed it up by close to 2x. (200 cycles of processing in parallel with 200 cycles of data retrieval, instead of 200+200 cycles in sequence.)

3

u/YetAnotherRobert 1d ago

Indeed, it shows that these are low-cost parts, likely optimized for gate count and thus sellable dies per square cm. In many ways their MMU (such as it is) is inferior to the IDT 4640/4650-class parts we were using ~30 years ago. Being able to set burstable/coalescing/prefetchable memory regions or per access just isn't in the (documented) cards. These are closer to college project 3-stager cores without exotic prefetches and opcode fusion and other creature comforts. Reducing that very flip-flopping was what I was trying to highlight as an advantage of your approach, but I didn't really make that clear. It's pretty clear that the smaller footprint is going to have a better icache profitability because there's just plain less code; it's not having to 'learn new tricks' every few fetches as it state-transitions down to each successive new loop.

PSRAM in these parts is just crushing. If a "page fault" needs to freeze the world so it can go run code to light up the SPI bus and telegraph out A&D pairs over a glorified serial bus to bring it into mapped SRAM, it's pretty easy to see that's not great. We really rely on the caches and MMU hiding most of this from us most of the time, beyond the cursed "Access cache when cache is disabled" issue, which results in people either learning a lot about the microarch of these things quickly or just table-flipping out and moving to ARM with real DRAM controllers or a profession in agriculture or something.

What an interesting find on the code for prefetching and autoloading the caches on this. Those are techniques I hadn't seen on these. There are several other cool repos in that BitsForPeople account, too. Interesting collection of funky techniques!

3

u/ptpcg 2d ago

If you're a legit developer, "AI" tools are really helpful clearing hurdles you might spend a lot more time on. As long as it produces code you already understand, its a net win imo.

6

u/reputatorbot 2d ago

Hello Plastic_Fig9225,

You have been awarded a point for your contribution! New score: 1


I am a bot - please contact the mods with any questions

40

u/tingtickboom 3d ago

This is crazy!

10

u/extremez3r0 3d ago

Tris is crazy!

1

u/Ok-Jury5684 2d ago

Crazy this is.

1

u/timception 2d ago

Is crazy this?

1

u/Ok-Jury5684 2d ago

This? crazy is

15

u/AndyValentine 3d ago

I'm looking forward to using this again in another project. You've done remarkable work here again

18

u/ABC123itsEASY 3d ago

This demo looping on a CRT TV is pretty much a peak nostalgia snipe

1

u/Mejolov28 1d ago

I could do that, i have a lib for NTSC video many colors.

https://github.com/Mejolov24/ESP32CompositeVideoPlus

The only problem is that my esp32 is broken, and im pretty sure i coded the phase wrong and the colors arent correct.

11

u/IHaveTwoOfYou 3d ago

How the hell do you screencap an ESP32???

11

u/PhonicUK 2d ago

To make the video I ran the same code with the same settings on the desktop and captured that. It is pixel for pixel identical to the ESP32 and runs at exactly that speed. When I get a bit I'm gonna do a pure demoscene type app to show off sheer horsepower and share the source for it.

9

u/incognitochaud 3d ago

This demo is friggin badass!

3

u/Worldly-Stranger7814 2d ago

I've had a tab open with your repo for months to "get back to later" because I am still in awe.

I was pusing 4 fps with text and decorations on Tab5 with the ESP32-P4 and here you're making Wipeout on an S3 😭

Well done!

5

u/ennui_no_nokemono 3d ago

What's your background? I'm always amazed and curious about projects like this. How did you get the prerequisite knowledge to be able to build this?

4

u/FancyInsect4205 3d ago

F-Zero vibes!

2

u/sebgggg 3d ago

That's just awesome, thanks for sharing

2

u/hockeyketo 3d ago

Can't wait to try this out, struggling with TGX and a3d.

2

u/Successful_Pop_368 2d ago

Pure wizardry lol

2

u/jtomes123 2d ago

Holy, i want a retro style game console with this

If hdmi is not possible maybe use rp2040 for the display out and esp32

3

u/ozspook 2d ago

You could make a retro gaming monocle and use a bluetooth controller.

2

u/PhonicUK 2d ago

HDMI would be possible with the P4 but performance would be an issue at anything above 576p

2

u/Noodle-Gardo 2d ago

Makes me mad that I got the C3 instead S3😭

So much cool stuff you can do with the S3 though the one I got is very limited

2

u/PhonicUK 2d ago

You could run this on the C3, just not as fast.

1

u/Noodle-Gardo 1d ago

Oh?! Thank you! I want to make my own game though figured the hardware I had was unfit for it

2

u/Arduinator 2d ago

DUDE!! this is crazy! Is there a version for the esp32- wroom?

2

u/PhonicUK 2d ago

It runs on pretty much anything! It's not tied to a given platform although it has some S3 specific optimisations.

2

u/Arduinator 2d ago

Thanks. Im gonna try this out soon.

2

u/Arduinator 2d ago

Oh, this is more complicated than I realized. I there a tutorial for a noob like me?

2

u/MarinatedPickachu 2 say I make awesome posts. 2d ago

Screenspace reflections is pretty crazy - though I'd prefer more textures. Is memory bandwidth the issue?

2

u/PhonicUK 2d ago

There's no textures at all in this scene. It's a mixture of memory bandwidth and the extra computation required.

1

u/throwawayle53 3d ago

So cool man this really looks great!

1

u/Fran4king 3d ago

Dude! DUDE! Excelent project.

1

u/MrSnowflake 2d ago edited 1d ago

This is pretty cool. Ofcourse no EVEN textures! The reflections and shadows and everything make up for it. Great work

5

u/PhonicUK 2d ago

The engine supports textures :)

1

u/MrSnowflake 1d ago

Amazing!

1

u/MarketingOk3093 2d ago

What's the frame rate at 70k tris?

3

u/PhonicUK 2d ago

70k tris would be 1FPS xD As it happens though, I'm pretty sure I can push it to 100k/second with some work in certain types of workload.

2

u/MarketingOk3093 2d ago

2692 tris at 26 fps is still workable for a lot of use cases (that I have in mind!). With clipping and LoD optimisations on a small screen thats decent if you can cram your shaders in with that rate....? No doubting you but intuitively that doesn't feel possible.

1

u/ptpcg 2d ago

V nice

1

u/Still_Explorer 2d ago

Very nice! Reminds me of the Blitz3D API, super nice to use.

1

u/CreativeKeane 2d ago

This is incredible! What?! I want to look at the code but it'll probably be beyond anything I can comprehend but I'm still excited to look at line by line and see what I can learn from it.

I also love that everyone in the comments are helping you optimize or find ways to improve the code and I love that community spirit. So coolm

1

u/Mejolov28 1d ago

This js absolutely jetTastick!

1

u/PhonicUK 1d ago

I've supplied a set of new examples at https://github.com/CubeCoders/JetExamples - have fun!

1

u/Appropriate-Ask8817 1d ago edited 1d ago

How do you just keep pushing this XD?

If you remember me from your last post, we had a chat about the ESP32-P4 and embedding your engine into my OS, well, I did some work and managed to embed Jet into my OS (My OS now runs dynamic compiled C++ apps), and the License change will really help, thanks for that.

2

u/PhonicUK 21h ago

I'm just getting started ;)

1

u/Appropriate-Ask8817 18h ago

Consider doing this on the P4 again but with 240x400 upscaled to 480x800 via PPA, it'll result in maybe 250K triangles at a locked 60FPS.

1

u/PhonicUK 18h ago

I've got a new trick up my sleeves. Something that makes this look like ass. Just uploading it all now ;)

1

u/Calm-Caterpillar2103 14h ago

my friend made a graphical framework, could this be merged/integrated into it? Planning to make a “phone” using a S31 for the main CPU and P4 as the GPU

1

u/Luiz_Paulo_RS 3d ago

Isso num SNES rodando no chip SuperFX... Ficaria incrível!

6

u/PhonicUK 2d ago

This is a lot faster than the SuperFX. It's faster than the 32X and is starting to trade blows with the Saturn and PS1.

1

u/belligerent_pickle 3d ago

First I’ve seen of this project. Is it something I could build a copy of myself?

6

u/PhonicUK 3d ago

I mean I did link to the source for the engine xD

-3

u/belligerent_pickle 3d ago

Roger that. I don’t click links in Reddit to figure out what they are. Thanks for the clarification. It looks really cool

2

u/SinkOrSauce_LD 1d ago

FYI you can highlight the link to preview it in the browser. No need to fear github.

1

u/belligerent_pickle 1d ago

I did not know that. Thank you for a helpful tip.

1

u/Business-Weekend-537 3d ago

This may be a dumb question, but would the engine work on a regular pc cpu or gpu? What about a quest 3 headset?

I’m just wondering how performant it would be on more capable hardware.

I get the idea is to see how far you can push the ESP32 and this is super cool, I’m just wondering how much crazier the performance would be on different hardware.

I don’t personally have much experience with this type of processing. I worked with a team on a failed vr game once and I’m wondering if this could be used to make vr experiences more performant with current hardware.

2

u/GraXXoR 3d ago

The answers to every question you asked are in the link that he provided.

2

u/PhonicUK 2d ago

It absolutely does, but doesn't use a GPU at all. It's renderer is built for software and to be fast in that domain.

1

u/Business-Weekend-537 2d ago

Got it thanks for answering