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:
- https://hackage.haskell.org/package/keid-core-0.1.8.0/changelog
- https://hackage.haskell.org/package/keid-render-basic-0.1.8.0/changelog
- https://hackage.haskell.org/package/geomancy-0.2.5.0/changelog
- https://hackage.haskell.org/package/ktx-codec-0.0.2.0/changelog
Compatibility fixes:
- https://hackage.haskell.org/package/keid-frp-banana-0.1.1.0/changelog
- https://hackage.haskell.org/package/keid-geometry-0.1.1.3/changelog
- https://hackage.haskell.org/package/keid-resource-gltf-0.1.0.1/changelog
- https://hackage.haskell.org/package/keid-ui-dearimgui-0.1.2.1/changelog
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.