ayaros 2 days ago

I'm working on a web app in JS, the sort of thing I think people on here will enjoy and I'll definitely share it when it's presentable. It's all in JS, including the graphical aspects of it (I'm drawing to a canvas). For per-pixel rendering I have 4 levels of nested loops. I feel like I've done everything in my power to optimize the process, and it's still not at the ideal speed. Really, it ought to be about 5-10 times faster than it is now.

I've been thinking of getting it into WASM somehow - it's really just one function that needs to be replaced by a WASM module - but one of the things which has given me pause are benchmarks like this, as well as other anecdotes, that demonstrate I'd only get a minor speedup from WASM. I guess part of this is due to modern JS engines being so optimized.

Just about all of the relevant code is adding and subtracting integers, but theres's some Boolean logic. There's also some multiplication in there that I could eliminate if I really had to, although at this point I'm not certain it would make a significant difference. Most of the data that contributes to the value of each pixel is spread across a tree of objects, and there's some polymorphism involved too. The point is it's not like I'm just dealing with a bunch of contiguous data in memory that is easily cached by the CPU, which might speed things up considerably. I guess I'm just venting now, lamenting a nightmare of my own creation...

2
catapart 2 days ago

For the record - I can't imagine any tangible value in writing a graphics engine (and form library) for an environment that was literally designed as a graphics engine for forms. But, that disclaimer aside, I've considered WASM for graphics for a game engine and I have had some trial success with allocating a shared array buffer, writing to it in WASM and reading from it at a js-friendly rate of 30-60 fps (whatever requestAnimationFrame is serving). The idea being that you treat the shared array buffer as your "screen" and write to it from the WASM as if it were an image buffer, while on the JS side you read it as an image buffer onto the canvas. Gives you a nice, finite, bandwidth and an easy to scale maximum for how much data you can pass through, and neither JS nor WASM ever has to pass any time-critical data.

Of course, this setup leaves a very thin JS client - it's essentially just rendering the same image over and over. So if you've already got everything done in JS, it would mean porting all of your functional code over to something that can compile into WASM. Not a huge lift, but not fun either.

And then, on the other hand, if it's just rendering you're seeing as a bottleneck, JS works pretty well with webGL and webGPU for simple form rendering. A little loop unrolling might be a lot easier than porting the rest of the app.

Anyway, best of luck on it!

ayaros 1 day ago

The graphics side of my program is structured as a tree of objects. The root is the entire display. Each object contains zero or more children, and masks those children so they only appear within the rectangular bounds of the object. If an object has an image class, and there are several of these using different data structures - then it further masks its children according to the image's alpha channel. I also have some blending modes that can be turned on. Any object can have a blending mode that affects appearance of the object based on the image of the object, or the images below it.

Is that something I could port to WebGL? Can objects contain or be parents of other objects in that way, and mask thier children to only appear directly in front of them? I have done a tiny bit of WebGL for a CS assignment but it was ages ago. Hopefully once I make this public I'll be able to get some feedback on all this. I don't want to say too much more as it would begin to spoil things!

catapart 1 day ago

It doesn't sound like you have an architecture that is particularly conducive to renderer-based programming, but that's not to say it's a difficult or complicated port. That all depends on the details.

When going to a dedicated graphics renderer, the biggest "change" from standard dev is "unrolling" your loops. So, right now, you have a bunch of parents and children and parents inform children. This is not, at all, different from a game engine which allows nesting of render objects (very common). Usually, the "transform" of a parent mesh is added to the transform of its children, for a very basic example.

If you're working in 3D, there's a bit of a complication with alpha channels because not only do you have to worry about blending, but you also have to deal with the intersection of alpha objects - simple front-to-back-then-back-to-front rendering won't cover that. But since you're just doing forms, you can be sure that all of your alpha content will be rendered after all of your opaque content, and blended according to its mode at that time, so it's a fairly straightforward version of "unrolling" that you would need.

But all of that is to say: no, WebGL (or any graphics API) won't have any kind of built in way to manage nesting objects. Those relations would have to be defined by you and uploaded to the GPU and then referenced separately, rather than looping through each parent and drilling down into the objects. It's just a different type of problem-solving. Rather than dealing with things per-object, you're dealing with them per-pixel (or pixel group). So you tell each pixel how it should render, based on what objects it is expected to describe at that pixel, and then let them all do their work at the exact same time. It's less "layer painting" and more "arrange and stamp".

lerp-io 1 day ago

have you done any benchmarks to compare wasm performance with js? i have been doing some benchmark test cases after a large refactor of my game engine written in typescript that is built entirely on top of shared buffer arrays (using ECS pattern or so it’s called i guess). i’m done benching for a while now and am ok with results but i have also compared my handcrafted quadtree sort/query algorithm with off the shelf rust implementation that matched the test requirements and saw only 30% gain and figured anything lower than that would not be worth the effort assuming rust compiled to wasm is same or slower and never bothered to test compiled wasm because i could not figure out how to share my buffer arrays with the rust code using wasm bindgen fast enough to bother spending more time on more benching lol

catapart 1 day ago

Nothing rigorous at all, no. I did do a lot of practical tests, as a way to compare what I had been doing to what was planned to be done, but I've found js is plenty performant for the types of CPU work I needed done, which is the big benefit of WASM.

Since I've mostly focused on rendering, there's just not much that really should be done on the CPU side. But I do have plans to extend my renderer to have a CPU pipeline which doesn't rely on WebGPU or WebGL, and I imagine I'll be doing quite a bit of benchmarking there. Forcing the CPU to render is going to make js choke - it's just a matter of how much it takes. I imagine any forward rendering will be fine, but there's no chance it does path tracing worth a damn. So somewhere between "textures" and "realtime global illumination" I expect I might have a lot of data about what WASM can do to help in that arena (if anything; though I am expecting it to help quite a bit, especially with stuff like SIMD).

On the other hand, entirely, I have been using the Rapier physics engine since it has a javascript version and as far as I know it heavily utilizes WASM for its physics calculations, so there might be some good benchmarking for WASM benefits surrounding that library.

refulgentis 1 day ago

I had a fun 2 years where I basically had 800 critical lines of math code that had to be on every platform, one version in the native language of the platform, and one version as performance optimal as possible.

I was absolutely stunned to find that on web WASM ~= JS which altogether weren't far off from a naive C++ stdlib port. I had to make custom data structures instead of using stdlib ones to really get performance significantly different . IIRC my most optimized C++ converted to WASM was about 70% the speed of JS.

com2kid 1 day ago

Don't write every pixel by hand to the screen. At high resolutions that just won't work. No modern UI does that anymore, they all use acceleration of some type. I ran into this trap when I tried to write a simple canvas game engine.

Just caching blobs of things that don't change frame to frame and got me a 5x-8x speedup, even if where I render those things at changes frame to frame.

ayaros 1 day ago

Trust me, I learned this pretty quickly. Areas are only queued when the display has to chnage in some way. There's also a bunch of prepratation before drawing a region of the screen; only the objects that appear within the specified bounds are even considered.