MetalConverter320/ConverterIIGS320.playground/Sources/Converter.swift

272 lines
11 KiB
Swift

/*
To run this demo under XCode 9.x or later, minor editing is required.
*/
import AppKit
import MetalKit
import simd
// We need to pass the 16 color tables as 1D array of UInt16's to the
// metal shader. Each color table has 16 color entries of size 2 bytes.
class ExtractIIgsGraphicData {
var iigsBitmap: [UInt8]?
var colorTables: [UInt16]?
var scbs: [UInt8]?
public init?(_ url: URL) {
// Each scanline is 160 bytes. Each byte consists of 2 "pixels".
// There are 200 scanlines in a 320x200 IIGS graphic
iigsBitmap = [UInt8](repeating: 0, count: 160*200)
// First load the entire file
// Extract the first 160x200 = 32 000 bytes - this is the bitmap
// Then extract the next 256 bytes - this is the SCB table only 200 required
// Then extract the last 512 bytes - 16 color tables = 16 x 32 bytes
var rawData: Data? = nil
do {
try rawData = Data(contentsOf: url)
}
catch let error {
print("Error", error)
return nil
}
var range = Range(0..<32000)
rawData?.copyBytes(to: &iigsBitmap!, from: range)
scbs = [UInt8](repeating: 0, count: 256)
range = Range(32000..<32256)
rawData?.copyBytes(to: &scbs!, from: range)
range = Range(32256..<32768)
colorTables = [UInt16](repeating:0, count: 256)
var buffer512 = [UInt8](repeating:0, count: 512)
rawData?.copyBytes(to: &buffer512, from: range)
var index = 0
// On the IIGS, UInt16 is in little-endian format.
for k in stride(from: 0, to: 512, by: 2) {
colorTables![index] = UInt16(buffer512[k]) + (UInt16(buffer512[k+1]) << 8)
// Checked! color table entries are correct.
//print(colorTables![index], terminator: " ")
index += 1
//if index % 16 == 0 {
// print()
//}
}
}
}
public class MetalViewRenderer: NSObject, MTKViewDelegate {
var queue: MTLCommandQueue?
var device: MTLDevice!
var rps: MTLRenderPipelineState!
var cps: MTLComputePipelineState!
var vertexBuffer: MTLBuffer!
var indexBuffer: MTLBuffer!
var bitMapBuffer: MTLBuffer!
var scbTablesBuffer: MTLBuffer!
var colorTablesBuffer: MTLBuffer!
var outputTexture: MTLTexture!
public init?(device: MTLDevice) {
super.init()
self.device = device
queue = device.makeCommandQueue()
createBuffers()
buildPipelineStates()
guard let texture = createTexture()
else {
print("Texture could not be created")
return nil
}
outputTexture = texture
}
func createBuffers() {
let myBundle = Bundle.main
let assetURL = myBundle.url(forResource: "ANGELFISH",
withExtension:"SHR")
let graphicsExtractor = ExtractIIgsGraphicData(assetURL!)!
let bmData = graphicsExtractor.iigsBitmap!
bitMapBuffer = device!.makeBuffer(bytes: bmData,
length: MemoryLayout<UInt8>.stride * bmData.count,
options: [])
let colorTables = graphicsExtractor.colorTables!
let numberOfColorEntries = colorTables.count
let sizeOfColorTables = MemoryLayout<UInt16>.stride * numberOfColorEntries
colorTablesBuffer = device!.makeBuffer(bytes: colorTables,
length: sizeOfColorTables,
options: [])
let scbTable = graphicsExtractor.scbs!
scbTablesBuffer = device!.makeBuffer(bytes: scbTable,
length: MemoryLayout<UInt8>.stride * scbTable.count,
options: [])
// size = 16 bytes; alignment=8; stride=16
struct Vertex {
var position: packed_float2
var texCoords: packed_float2
}
// Note: both the position & texture coordinates are already
// normalized to the range [-1.0, 1.0] & [0.0, 1.0] respectively.
// total size = 64 bytes
let quadVertices: [Vertex] =
// clockwise - triangle strip; origin of tex coord system is upper-left.
[
Vertex(position: [-0.75, -0.75], texCoords: [ 0.0, 1.0 ]), // v0
Vertex(position: [-0.75, 0.75], texCoords: [ 0.0, 0.0 ]), // v1
Vertex(position: [ 0.75, -0.75], texCoords: [ 1.0, 1.0 ]), // v2
Vertex(position: [ 0.75, 0.75], texCoords: [ 1.0, 0.0 ]), // v3
]
vertexBuffer = device!.makeBuffer(bytes: quadVertices,
length: MemoryLayout<Vertex>.stride * quadVertices.count,
options: [])
// total size = 16 bytes.
let indices: [UInt16] = [0, 1, 2, 2, 1, 3]
indexBuffer = device!.makeBuffer(bytes: indices,
length: MemoryLayout<UInt16>.stride * indices.count,
options: [])
}
func buildPipelineStates() {
let path = Bundle.main.path(forResource: "Shaders",
ofType: "metal")
let input: String?
var library: MTLLibrary?
do {
input = try String(contentsOfFile: path!,
encoding: String.Encoding.utf8)
library = try device!.makeLibrary(source: input!,
options: nil)
let kernel = library!.makeFunction(name: "convert320")!
cps = try device!.makeComputePipelineState(function: kernel)
}
catch let e {
Swift.print("\(e)")
}
let vertex_func = library!.makeFunction(name: "vertexShader")
let frag_func = library!.makeFunction(name: "fragmentShader")
// Setup a render pipeline descriptor
let rpld = MTLRenderPipelineDescriptor()
rpld.vertexFunction = vertex_func
rpld.fragmentFunction = frag_func
// Note: the kernel return each pixel as rgba but the render pipeline
// insists it's bgra. Weird.
rpld.colorAttachments[0].pixelFormat = .bgra8Unorm
do {
try rps = device!.makeRenderPipelineState(descriptor: rpld)
}
catch let error {
Swift.print("\(error)")
}
}
// Instantiate the output texture object and generate its contents
// so that the render encoder could use.
func createTexture() -> MTLTexture? {
let width = 320
let height = 200
let textureDesc = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .rgba8Unorm,
width: Int(width),
height: Int(height),
mipmapped: false)
// Must be read and write.
textureDesc.usage = [.shaderWrite, .shaderRead]
textureDesc.resourceOptions = .storageModeManaged
let texture = device.makeTexture(descriptor: textureDesc)
if let commandBuffer = queue?.makeCommandBuffer() {
commandBuffer.addCompletedHandler {
(commandBuffer: MTLCommandBuffer) -> Void in
if commandBuffer.status == .error {
Swift.print(commandBuffer.error!)
}
else if commandBuffer.status == .completed {
Swift.print("Texture was generated successfully")
}
}
let commandComputeEncoder = commandBuffer.makeComputeCommandEncoder()
print("Generate Texture")
commandComputeEncoder.setComputePipelineState(cps)
commandComputeEncoder.setTexture(texture,
at: 0)
commandComputeEncoder.setBuffer(bitMapBuffer,
offset: 0,
at: 0)
commandComputeEncoder.setBuffer(scbTablesBuffer,
offset: 0,
at: 1)
commandComputeEncoder.setBuffer(colorTablesBuffer,
offset: 0,
at: 2)
// Use the max # of threads available for parallel processing.
let w = cps.threadExecutionWidth
let h = cps.maxTotalThreadsPerThreadgroup / w
let threadsPerThreadgroup = MTLSizeMake(w, h, 1)
let threadgroupsPerGrid = MTLSizeMake((texture.width + w - 1) / w,
(texture.height + h - 1) / h,
1)
// Execute the kernel function
// Note: boundary checks are necessary in the compute shader
// unless we use the alternative method
// dispatchThreads:threadsPerThreadgroup:
commandComputeEncoder.dispatchThreadgroups(threadgroupsPerGrid,
threadsPerThreadgroup: threadsPerThreadgroup)
commandComputeEncoder.endEncoding()
commandBuffer.commit()
}
return texture
}
// Implementation of the 2 MTKView delegate protocol functions.
public func mtkView(_ view: MTKView,
drawableSizeWillChange size: CGSize) {
}
// drawInMTKView:
public func draw(in view: MTKView) {
if let rpd = view.currentRenderPassDescriptor,
let drawable = view.currentDrawable,
let commandBuffer = queue?.makeCommandBuffer() {
view.clearColor = MTLClearColorMake(0.5, 0.5, 0.5, 1.0)
// Render the generated graphic.
let commandRenderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: rpd)
commandRenderEncoder.setRenderPipelineState(rps)
commandRenderEncoder.setVertexBuffer(vertexBuffer,
offset: 0,
at: 0)
commandRenderEncoder.setFragmentTexture(outputTexture,
at: 0)
commandRenderEncoder.drawIndexedPrimitives(type: MTLPrimitiveType.triangleStrip,
indexCount: indexBuffer.length/MemoryLayout<UInt16>.size,
indexType: MTLIndexType.uint16,
indexBuffer: indexBuffer,
indexBufferOffset: 0)
commandRenderEncoder.endEncoding()
commandBuffer.present(drawable)
commandBuffer.commit()
}
}
}