Demo Link: https://conorpo.github.io/triangular-array-cellular-automaton
- [✔️] Add a way to change initialization - ETA: 1 hour
- [✔️] Get rid of the atomic operations
- [✔️] Display the rule number - ETA: 2 hours
- [✔️] Add a way to change the rule number - ETA: combien with above
- [✔️] Add View Controls
- [✔️] Add a way to change the color scheme - ETA: 2 hours
- [❌] Fix filtering / multisampling - ETA: idk whats going on
- [❌] Add history - ETA: 3 hours
- [✔️] Debug View - ETA: 1 hour
Once again the codebase didn't turn out perfect, but it's a lot better than the last one. WebGPU resources are tough to modularize cleanly; I separated them by shader which made it hard to automatically update the bindings.
I decided on the following abstraction model; but for my next project, I would do things a bit differently (see Pros/Cons below).
- Resource Container
webgpu_resource
- The webgpu resource objectlocal_resource
- The local resource objectupdate()
- Updates the webgpu resource with the local resourcecreate()
- Creates the webgpu resource
- Bindgroup Container
_bindgroup
- The bindgroup object, memoizedlayout
- The bindgroup layout objectdirty
- Whether the bindgroup needs to be recreatedbindgroup
- The bindgroup getter, recreates the bindgroup if dirtycreate_layout()
- Creates the bindgroup layoutcreate_bindgroup()
- Creates the bindgroup
- Pipeline Container
shader
- The shader modulelayout
- The pipeline layoutpipeline
- The pipelinecreate_layout()
- Creates the pipeline layoutcreate()
- Creates the shader module and pipeline
First of all, it's not exactly strictly typed, webgpu_resource can be a Buffer, Texture, or an Array of either of those. In addition, webgpu_resource starts as null. I decided it would be my responsibility to make sure resources are created before they are used, this allowed me to define project structure in modules before the device is created.
But at the end of the day, it abstracted the parts I needed and provided some useful functionality. For example, GPU resources could be updated with a simple .update()
call and local resources could be directly modified by other modules.
In terms of the bindgroups, instead of automatically recreating them whenever a resource was re-created (slow if many resources are re-created during the frame), it memoizes the bindgroup, only recreating it if it's been marked dirty by a ResourceContainer .create()
call. The bindgroup also creates its own layout. Storing the layouts on the bindgroups means resources can be used in multiple ways (e.g. GPUBuffer with the usage flag STORAGE can be bound as "storage" or "read-only-storage" depending on the bindgroup it's used in). .create_layout()
is expected to have already been called before .create_bindgroup()
or .bindgroup
is called. Once again I chose this approach to lower validation costs, and encourage proper project initialization.
Finally, the pipeline container is the simplest as the layout is never gonna need to be recreated (for my projects), and the shader module and pipeline are created together, which allows for hot reloading of the shader module.
I think next time I would be a bit stricter about type-checking the webgpu_resource, having separate types for each type of resource that extend some ResourceContainer base class. Instead of allowing resource containers to have arrays, I would instead choose to have arrays of resource containers. Descriptors could be moved outside of the resource container for reuse in create functions. Update functions could also be moved outside and reused by including the ResourceContainer as a parameter. These array changes could be applied to BindgroupContainers as well, it's not 100% clean, but arrays of bindgroups seem like a rare use case. I plan to explore further limitations of this abstraction model in future projects.
Group 0 | Group 1 | Group 2 | Group 3 | |
---|---|---|---|---|
Init Stage | RuleInfo + Ruleset | Random Seeds | ||
Iterate Stage | RuleInfo + Ruleset | Input CA Texture + Output CA Texture, Input IterateSettings + Output IterateSettings (Ping Pong) | ||
Render Stage | View Info | CA Texture | ||
Debug View Stage | View Info | RuleInfo |
Iterate Stage computes one iteration of the CA, it is run once for every row in the CA Texture.
Render Stage renders the CA Texture to the screen, offsetting odd rows by half a cell.