For a Metal application, I would like to create and initalize texture object globally so it will not have to be optional, and will already be ready to use once viewDidLoad
is called. To do this, I need a MTLTextureDescriptor
with the correct values inline. It has to be inline because I do not want to create a global descriptor that will only be used once, and since this file is not main.swift
, out of line code at the top level will not be accepted. I have:
let Device = MTLCreateSystemDefaultDevice()!, TerrainTexture = Device.makeTexture(descriptor: ({
let Descriptor = MTLTextureDescriptor()
Descriptor.textureType = .type2DArray
Descriptor.arrayLength = TERRAIN_TEXTURES_COUNT
Descriptor.width = TERRAIN_TEXTURE_SIZE
Descriptor.height = TERRAIN_TEXTURE_SIZE
Descriptor.mipmapLevelCount = TERRAIN_TEXTURE_MIPMAP_LEVEL_COUNT
return Descriptor
})())!
I define a closure, and call it to get a descriptor to get a texture, all inline, since the initializer of MTLTextureDescriptor
does not have parameters for these values, so I need to set them manually. But is making a closure inline and calling it really the cleanest way to simply initialize the values of a struct or class to constant values inline? Is there a nicer way to do this?
2 Answers 2
You said:
It has to be inline because I do not want to create a global descriptor that will only be used once
The provided pattern, where you call makeTexture
with a parameter that is a closure is a bit hard to read. Instead, you can initialize the whole MTLTexture
with a closure:
let device = MTLCreateSystemDefaultDevice()!
let terrainTexture: MTLTexture = {
let descriptor = MTLTextureDescriptor()
descriptor.textureType = .type2DArray
descriptor.arrayLength = TERRAIN_TEXTURES_COUNT
descriptor.width = TERRAIN_TEXTURE_SIZE
descriptor.height = TERRAIN_TEXTURE_SIZE
descriptor.mipmapLevelCount = TERRAIN_TEXTURE_MIPMAP_LEVEL_COUNT
return device.makeTexture(descriptor: descriptor)!
}()
That achieves the same thing, avoiding a global descriptor, but is a little more readable.
According to the Swift API Design Guidelines, it's recommended to:
- Use camel case and start with a lowercase letter for the names of variables.
- Avoid including the type of the variable in its name
name variables [...] according to their roles, rather than their type constraints.
So use terrain
instead of TerrainTexture
, and device
instead of Device
.
- Avoid force unwrapping Optionals unless you are sure it's never going to fail or if you really want the app to crash without providing you with any debugging information. Optional binding gives you the opportunity to give an alternate route to your code other than crashing. In this case, we could just throw an error using
assert()
,assertFailiure()
,precondition()
,preconditionFailiure
, orfatalError
. Let's use the latter because it allows us to print a string in the console before the app terminates, and it works for all app optimization levels in all build configurations.:
guard let device = MTLCreateSystemDefaultDevice() else {
fatalError("Couldn't get a reference to the preferred default Metal device object.")
}
To conceal the intermediate steps needed to build objects in your code base, use the Factory pattern π.
For objects that take an empty initializer, like a
MTLTextureDescriptor
, you could use the Builder Pattern. It emphasizes the separation between the phase of building the object to your liking and the phase of actually using it. You could define a custom convenience initializer, but the Builder pattern gives you more freedom in the number of properties to initialize and the order of assigning values to the different properties of the object.
We could use this pattern to build the descriptor
:
extension MTLTextureDescriptor: Buildable {}
let descriptor = MTLTextureDescriptor.builder()
.textureType(.type2DArray)
.arrayLength(TERRAIN_TEXTURES_COUNT)
.width(TERRAIN_TEXTURE_SIZE)
.height(TERRAIN_TEXTURE_SIZE)
.mipmapLevelCount(TERRAIN_TEXTURE_MIPMAP_LEVEL_COUNT)
.build()
This is possible via the power of Protocols, Dynamic member lookup and Keypaths:
@dynamicMemberLookup
class Builder<T> {
private var value: T
init(_ value: T) { self.value = value }
subscript<U>(dynamicMember keyPath: WritableKeyPath<T, U>) -> (U) -> Builder<T> {
{
self.value[keyPath: keyPath] = 0γγ«
return self
}
}
func build() -> T { self.value }
}
protocol Buildable { init() }
extension Buildable {
static func builder() -> Builder<Self> {
Builder(Self())
}
}
This talk and this article will help you understand this pattern better.
For a more comprehensive implementation of the Builder pattern, refer to this GitHub repository.
Kintsugi time
With the Builder pattern in a separate file, it's time to put together all the above remarks.
Here is how your code looks now after some much-needed makeover:
extension MTLTextureDescriptor: Buildable {}
let terrain: MTLTexture = {
guard let device = MTLCreateSystemDefaultDevice() else {
fatalError("Couldn't get a reference to the preferred default Metal device object.")
}
let descriptor = MTLTextureDescriptor.builder()
.textureType(.type2DArray)
.arrayLength(TERRAIN_TEXTURES_COUNT)
.width(TERRAIN_TEXTURE_SIZE)
.height(TERRAIN_TEXTURE_SIZE)
.mipmapLevelCount(TERRAIN_TEXTURE_MIPMAP_LEVEL_COUNT)
.build()
guard let texture = device.makeTexture(descriptor: descriptor) else {
fatalError("Couldn't make a new texture object.")
}
return texture
}()
This way your code is safer, better readable, and has stronger separation of concerns.
terrainTexture
rather thanTerrainTexture
). Uppercase is reserved for type names (e.g. classes, structs, enum types, etc.). Also, the un-namespaced, screaming snake case of those constants (e.g.TERRAIN_TEXTURE_SIZE
vsConstants.terrainTextureSize
or whatever) is distinctly unswifty, too, feeling like it was inherited from an old ObjC codebase. \$\endgroup\$