2023-04-07

This is a big one, incorporating usability improvements from two jams and a workshop.

https://gitlab.com/keid/meta/-/raw/main/resolvers/keid-engine-2023-04-07.yaml (GHC 9.2.7 / LTS-20.17)

Major updates:

Compatibility fixes:

Code denoising

It all started with putting Engine.Worker spawns into a ResourceT context and dropping manual key management. Then the Resource.Buffer got the same treatment. The amount of code noise dropped significantly. As a side-effect, using resource collections is now even simpler with traverse and unnecessary wrappers removed.

Another minor piece of boilerplate that was removed was asking for context. Most of the allocation functions will now ask for it themselves.

Generically…

The release was almost ready to go, but one thing that annoyed me remained: the necessity to write special wrappers for pipelines that use split instance buffers (i.e. main set of instance attributes and Transform). While creating buffers themselves was just a matter of using Applicative, making observers for each model isn’t that fun and is prone to errors.

So, a new (experimental for now) API was added: Resource.Model.Observer. The two observer functions can replace specialized initialization and buffer updates. Generics provide instances for collections, while the base case is handled by Buffer 'Coherent and Storable.Vector.

An example module shows how to prepare model data using new tools.

Just one more thing

Inspired by this, another long-known source of boilerplate got dismantled: forming pipeline vertex attribute bindings.

Config.cVertexInput field now can be populated with Graphics.vertexInput using type parameters from the Pipeline. From the looks of it, the field may be removed entirely in the next major version.

There are two new classes to support this derivation: HasVkFormat and HasVertexInputBindings.

For per-vertex attributes, HasVkFormat can (and should) be derived from Generic. HasVertexInputBindings would be used from the Model.Vertex instance. Or (), if the vertex positions are stored in the shader code instead.

For per-instance attributes, HasVertexInputBindings should be written with the help of instanceFormat. Transform has a default instance for models that only use transforms. And so does () when a model doesn’t use instance attributes at all. A manual HasVkFormat instance may be used to compress multiple record fields into a single binding (e.g. offset, size :: Vec2 -> vec4 offsetSize).

Fixes

Vertex data

Using the new classes exposed a primordial mistake in the pipeline vertex bindings. All of the pipelines used the vertices type parameter for the “extra” vertex attributes, assuming a position is always present. This wasn’t true for many pipelines, like Sprite, that use instanced drawing to position objects. Thus the vertexInput generated incorrect binding locations and Bound was enforcing the wrong attribute constraints. This was fixed by wrapping the attributes in Model.Vertex or its aliases (Vertex2d/Vertex3d) where necessary and putting () where no attributes are expected.

Deriving Storable

Somewhere during the attribute shuffle, the Storable instances derived from GStorable plugin caught inserting C-struct padding and shuffling attributes. While this may be a correct implementation for FFI types, the shader blocks are using a somewhat different layout. And vertex attributes are packed into arrays without any concern for alignment at all.

Having been spoiled by generics it is now difficult to spell out the offsets and sizes and stuff for correct Storable instances. Instead, I adopted the Block class from the unpublished package inside the codex project by Edward Kmett and grafted the functions for packed data to it.

Despite not using the plugin support the GStorable-derived classes enjoy, benchmarks show that Storable instances derived this way are competitive with the hand-written implementation IF you don’t stack multiple levels of types with generic instances.

So, a new recommendation is just using Storable instances derived from the generic/base Block instances via one of the layout wrappers:

  • Vertex/instance attributes: deriving Storable via (Packed ColorAndStuff).
  • Uniform buffers: deriving Storable via (Std140 SceneStuff).
  • Storage buffers and Push constants: deriving Storable via (Std430 Stuff).

Legibility

Having spent a non-trivial amount of time reading validator messages and inspecting RenderDoc resources I added debug names to Image, Buffers, and other objects.

You can now ask/get resources right next to the draw calls, removing the “collect stuff”/“draw stuff” gap in the rendering code.