2
\$\begingroup\$

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?

\$\endgroup\$
1
  • \$\begingroup\$ Please forgive the unrelated observation, but in Swift, variable/property names always start with a lowercase letter (e.g. terrainTexture rather than TerrainTexture). 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 vs Constants.terrainTextureSize or whatever) is distinctly unswifty, too, feeling like it was inherited from an old ObjC codebase. \$\endgroup\$
    – Rob
    Sep 12, 2021 at 17:07

2 Answers 2

3
\$\begingroup\$

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.

\$\endgroup\$
3
\$\begingroup\$
  • 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, or fatalError. 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.

\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.