首页
统计
留言板
关于
Search
1
Rust 包装 objc Block
166 阅读
2
Flutter 调用 Rust 生成的 共享/静态 库
157 阅读
3
02. Rust 内存管理 Copy & Clone(上)
157 阅读
4
用 Rust 开发 iOS 应用(粗糙版)
104 阅读
5
纯 C 写个 iOS App(误)
99 阅读
默认分类
Rust
Apple
iOS
Swift
Android
emulator
NES
登录
/
注册
Search
标签搜索
Rust
iOS
NES
Swift
Android
杂项
limit
累计撰写
18
篇文章
累计收到
0
条评论
首页
栏目
默认分类
Rust
Apple
iOS
Swift
Android
emulator
NES
页面
统计
留言板
关于
搜索到
5
篇与
的结果
2023-03-29
用 Metal 画一个三角形(Swift 函数式风格)
用 Metal 画一个三角形(Swift 函数式风格)由于今年换了一份工作,平时上班用得语言换成 Rust/OCaml/ReScript 啦,所以导致我现在写代码更倾向于写函数式风格的代码。 顺便试试 Swift 在函数式方面能达到啥程度。主要是我不会 Swift,仅仅为了好玩。创建工程随便创建个工程,小玩具就不打算跑在手机上了,因为我的设备是 ARM 芯片的,所以直接创建个 Mac 项目,记得勾上包含测试。构建 MTKView 子类现在来创建个 MTKView 的子类,其实我现在已经不接受这种所谓的面向对象,开发者用这种方式,就要写太多篇幅来描述一个上下文结构跟函数就能实现的动作。import MetalKit class MetalView: MTKView { required init(coder: NSCoder) { super.init(coder: coder) device = MTLCreateSystemDefaultDevice() render() } } extension MetalView { func render() { // TODO: 具体实现 } }我们这里给 MetalView extension 了一个 render 函数,里面是后续要写得具体实现。普通的方式画一个三角形先用常见的方式来画一个三角形class MetalView: MTKView { required init(coder: NSCoder) { super.init(coder: coder) device = MTLCreateSystemDefaultDevice() render() } } extension MetalView { func render() { guard let device = device else { fatalError("Failed to find default device.") } let vertexData: [Float] = [ -1.0, -1.0, 0.0, 1.0, 1.0, -1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0 ] let dataSize = vertexData.count * MemoryLayout<Float>.size let vertexBuffer = device.makeBuffer(bytes: vertexData, length: dataSize, options: []) let library = device.makeDefaultLibrary() let renderPassDesc = MTLRenderPassDescriptor() let renderPipelineDesc = MTLRenderPipelineDescriptor() if let currentDrawable = currentDrawable, let library = library { renderPassDesc.colorAttachments[0].texture = currentDrawable.texture renderPassDesc.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.5, blue: 0.5, alpha: 1.0) renderPassDesc.colorAttachments[0].loadAction = .clear renderPipelineDesc.vertexFunction = library.makeFunction(name: "vertexFn") renderPipelineDesc.fragmentFunction = library.makeFunction(name: "fragmentFn") renderPipelineDesc.colorAttachments[0].pixelFormat = .bgra8Unorm let commandQueue = device.makeCommandQueue() guard let commandQueue = commandQueue else { fatalError("Failed to make command queue.") } let commandBuffer = commandQueue.makeCommandBuffer() guard let commandBuffer = commandBuffer else { fatalError("Failed to make command buffer.") } let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDesc) guard let encoder = encoder else { fatalError("Failed to make render command encoder.") } if let renderPipelineState = try? device.makeRenderPipelineState(descriptor: renderPipelineDesc) { encoder.setRenderPipelineState(renderPipelineState) encoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1) encoder.endEncoding() commandBuffer.present(currentDrawable) commandBuffer.commit() } } } }然后是我们需要注册的 Shader 两个函数#include <metal_stdlib> using namespace metal; struct Vertex { float4 position [[position]]; }; vertex Vertex vertexFn(constant Vertex *vertices [[buffer(0)]], uint vid [[vertex_id]]) { return vertices[vid]; } fragment float4 fragmentFn(Vertex vert [[stage_in]]) { return float4(0.7, 1, 1, 1); }在运行之前需要把 StoryBoard 控制器上的 View 改成我们写得这个 MTKView 的子类。自定义操作符函数式当然不是指可以定义操作符,但是没有这些操作符,感觉没有魂灵,所以先定义个管道符代码实现precedencegroup SingleForwardPipe { associativity: left higherThan: BitwiseShiftPrecedence } infix operator |> : SingleForwardPipe func |> <T, U>(_ value: T, _ fn: ((T) -> U)) -> U { fn(value) }测试管道符因为创建项目的时候,勾上了 include Tests,直接写点测试代码,执行测试。final class using_metalTests: XCTestCase { // ... func testPipeOperator() throws { let add = { (a: Int) in return { (b: Int) in return a + b } } assert(10 |> add(11) == 21) let doSth = { 10 } assert(() |> doSth == 10) } }目前随便写个测试通过嘞。Functional Programming现在需要把上面的逻辑分割成小函数,事实上,因为 Cocoa 的基础是建立在面向对象上的,我们还是没法完全摆脱面向对象,目前先小范围应用它。生成 MTLBuffer先理一下逻辑,代码开始是创建顶点数据,生成 bufferfileprivate let makeBuffer = { (device: MTLDevice) in let vertexData: [Float] = [ -1.0, -1.0, 0.0, 1.0, 1.0, -1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0 ] let dataSize = vertexData.count * MemoryLayout<Float>.size return device.makeBuffer(bytes: vertexData, length: dataSize, options: []) }创建 MTLLibrary接着是创建 MTLLibrary 来注册两个 shader 方法,还创建了一个 MTLRenderPipelineDescriptor 对象用于创建 MTLRenderPipelineState,但是创建的 MTLLibrary 对象是一个 Optional 的,所以其实得有两步,总之先提取它再说吧fileprivate let makeLib = { (device: MTLDevice) in device.makeDefaultLibrary() }抽象 map 函数根据我们有限的函数式编程经验,像 Optional 这种对象大概率有一个 map 函数,所以我们自家实现一个,同时还要写成柯里化的(建议自动柯里语法糖化入常),因为这里有逃逸闭包,所以要加上 @escapingfunc map<T, U>(_ transform: @escaping (T) throws -> U) rethrows -> (T?) -> U? { return { (o: T?) in return try? o.map(transform) } }处理 MTLRenderPipelineState这里最终目的就是 new 了一个 MTLRenderPipelineState,顺带处理把程序的一些上下文给渲染管线描述器(MTLRenderPipelineDescriptor),譬如我们用到的着色器(Shader)函数,像素格式。最后一行直接 try! 不处理错误啦,反正出问题直接会抛出来的fileprivate let makeState = { (device: MTLDevice) in return { (lib: MTLLibrary) in let renderPipelineDesc = MTLRenderPipelineDescriptor() renderPipelineDesc.vertexFunction = lib.makeFunction(name: "vertexFn") renderPipelineDesc.fragmentFunction = lib.makeFunction(name: "fragmentFn") renderPipelineDesc.colorAttachments[0].pixelFormat = .bgra8Unorm return (try! device.makeRenderPipelineState(descriptor: renderPipelineDesc)) } }暂时收尾已经不想再抽取函数啦,其实还能更细粒度地处理,因为函数式有个纯函数跟副作用的概念,像 Haskell 里是可以用 Monad 来处理副作用的情况,这个主题留给后续吧。先把 render 改造一下fileprivate let render = { (device: MTLDevice, currentDrawable: CAMetalDrawable?) in return { state in let renderPassDesc = MTLRenderPassDescriptor() if let currentDrawable = currentDrawable { renderPassDesc.colorAttachments[0].texture = currentDrawable.texture renderPassDesc.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.5, blue: 0.5, alpha: 1.0) renderPassDesc.colorAttachments[0].loadAction = .clear let commandQueue = device.makeCommandQueue() guard let commandQueue = commandQueue else { fatalError("Failed to make command queue.") } let commandBuffer = commandQueue.makeCommandBuffer() guard let commandBuffer = commandBuffer else { fatalError("Failed to make command buffer.") } let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDesc) guard let encoder = encoder else { fatalError("Failed to make render command encoder.") } encoder.setRenderPipelineState(state) encoder.setVertexBuffer(device |> makeBuffer, offset: 0, index: 0) encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1) encoder.endEncoding() commandBuffer.present(currentDrawable) commandBuffer.commit() } } }然后再调用,于是就变成下面这副鸟样子class MetalView: MTKView { required init(coder: NSCoder) { super.init(coder: coder) device = MTLCreateSystemDefaultDevice() device |> map { makeLib($0) |> map(makeState($0)) |> map(render($0, self.currentDrawable)) } } }最后执行出这种效果
2023年03月29日
61 阅读
0 评论
0 点赞
2022-11-23
Swift 学习
介绍最近打算重新学习一下 Swift,因为之前一直使用 objc 的思维模式写 Swift,另外想进一步了解 Swift 语言的设计实现,应该能丰富我的认知。特意找了个旧设备装上 macOS 10.14.6 系统,并安装上 Xcode 11 ,新版本语法特性太多,暂时先不考虑,只从早期 ABI 稳定又稍微升级过的 5.1 版本开始。swift -version # Apple Swift version 5.1.3 (swiftlang-1100.0.282.1 clang-1100.0.33.15) # Target: x86_64-apple-darwin18.7.0Swift 编译流程开始之前,先了解下 Swift 语言的编译流程图中命令其实是独立的,这样画只是说明它经历过哪些环节,先是从 Swift 代码开始,编译器会对代码进行分词,解析,生成抽象语法树,这期间会进行语义分析,譬如类型检查之类的操作。接着就是把生成的 ast 转成 sil,既然有 sil,那自然就是优化 sil,完了就生成 LLVM 的 IR 码,然后通过 LLVM 把得到的 IR 码转成机器代码(期间会进行编译,链接库等操作,然后才生成对应平台的二进制文件)。Swift 基本命令swiftc 这个是编译器的命令,可以通过 swiftc --help 来查看有哪些参数。OVERVIEW: Swift compiler USAGE: swiftc MODES: -dump-ast Parse and type-check input file(s) and dump AST(s) -dump-parse Parse input file(s) and dump AST(s) -dump-scope-maps <expanded-or-list-of-line:column> Parse and type-check input file(s) and dump the scope map(s) -dump-type-info Output YAML dump of fixed-size types from all imported modules -dump-type-refinement-contexts Type-check input file(s) and dump type refinement contexts(s) -emit-assembly Emit assembly file(s) (-S) -emit-bc Emit LLVM BC file(s) -emit-executable Emit a linked executable -emit-imported-modules Emit a list of the imported modules -emit-ir Emit LLVM IR file(s) -emit-library Emit a linked library -emit-object Emit object file(s) (-c) -emit-sibgen Emit serialized AST + raw SIL file(s) -emit-sib Emit serialized AST + canonical SIL file(s) -emit-silgen Emit raw SIL file(s) -emit-sil Emit canonical SIL file(s) -index-file Produce index data for a source file -parse Parse input file(s) -print-ast Parse and type-check input file(s) and pretty print AST(s) -resolve-imports Parse and resolve imports in input file(s) -typecheck Parse and type-check input file(s) OPTIONS: -api-diff-data-dir <path> Load platform and version specific API migration data files from <path>. Ignored if -api-diff-data-file is specified. -api-diff-data-file <path> API migration data is from <path> -application-extension Restrict code to those available for App Extensions -assert-config <value> Specify the assert_configuration replacement. Possible values are Debug, Release, Unchecked, DisableReplacement. -continue-building-after-errors Continue building, even after errors are encountered -debug-info-format=<value> Specify the debug info format type to either 'dwarf' or 'codeview' -debug-info-store-invocation Emit the compiler invocation in the debug info. -debug-prefix-map <value> Remap source paths in debug info -disable-autolinking-runtime-compatibility-dynamic-replacements Do not use autolinking for the dynamic replacement runtime compatibility library -disable-autolinking-runtime-compatibility Do not use autolinking for runtime compatibility libraries -disable-migrator-fixits Disable the Migrator phase which automatically applies fix-its -driver-time-compilation Prints the total time it took to execute all compilation tasks -dump-migration-states-dir <path> Dump the input text, output text, and states for migration to <path> -dump-usr Dump USR for each declaration reference -D <value> Marks a conditional compilation flag as true -embed-bitcode-marker Embed placeholder LLVM IR data as a marker -embed-bitcode Embed LLVM IR bitcode as data -emit-dependencies Emit basic Make-compatible dependencies files -emit-loaded-module-trace-path <path> Emit the loaded module trace JSON to <path> -emit-loaded-module-trace Emit a JSON file containing information about what modules were loaded -emit-module-interface-path <path> Output module interface file to <path> -emit-module-interface Output module interface file -emit-module-path <path> Emit an importable module to <path> -emit-module Emit an importable module -emit-objc-header-path <path> Emit an Objective-C header file to <path> -emit-objc-header Emit an Objective-C header file -emit-tbd-path <path> Emit the TBD file to <path> -emit-tbd Emit a TBD file -enable-library-evolution Build the module to allow binary-compatible library evolution -enforce-exclusivity=<enforcement> Enforce law of exclusivity -fixit-all Apply all fixits from diagnostics without any filtering -framework <value> Specifies a framework which should be linked against -Fsystem <value> Add directory to system framework search path -F <value> Add directory to framework search path -gdwarf-types Emit full DWARF type info. -gline-tables-only Emit minimal debug info for backtraces only -gnone Don't emit debug info -g Emit debug info. This is the preferred setting for debugging with LLDB. -help Display available options -import-underlying-module Implicitly imports the Objective-C half of a module -index-file-path <path> Produce index data for file <path> -index-ignore-system-modules Avoid indexing system modules -index-store-path <path> Store indexing data to <path> -I <value> Add directory to the import search path -j <n> Number of commands to execute in parallel -L <value> Add directory to library link search path -l<value> Specifies a library which should be linked against -migrate-keep-objc-visibility When migrating, add '@objc' to declarations that would've been implicitly visible in Swift 3 -migrator-update-sdk Does nothing. Temporary compatibility flag for Xcode. -migrator-update-swift Does nothing. Temporary compatibility flag for Xcode. -module-cache-path <value> Specifies the Clang module cache path -module-link-name <value> Library to link against when using this module -module-name <value> Name of the module to build -nostdimport Don't search the standard library import path for modules -num-threads <n> Enable multi-threading and specify number of threads -Onone Compile without any optimization -Osize Compile with optimizations and target small code size -Ounchecked Compile with optimizations and remove runtime safety checks -output-file-map <path> A file which specifies the location of outputs -O Compile with optimizations -o <file> Write output to <file> -parse-as-library Parse the input file(s) as libraries, not scripts -parse-sil Parse the input file as SIL code, not Swift source -parseable-output Emit textual output in a parseable format -profile-coverage-mapping Generate coverage data for use with profiled execution counts -profile-generate Generate instrumented code to collect execution counts -profile-use=<profdata> Supply a profdata file to enable profile-guided optimization -remove-runtime-asserts Remove runtime safety checks. -require-explicit-availability-target <target> Suggest fix-its adding @available(<target>, *) to public declarations without availability -require-explicit-availability Require explicit availability on public declarations -Rpass-missed=<value> Report missed transformations by optimization passes whose name matches the given POSIX regular expression -Rpass=<value> Report performed transformations by optimization passes whose name matches the given POSIX regular expression -runtime-compatibility-version <value> Link compatibility library for Swift runtime version, or 'none' -sanitize-coverage=<type> Specify the type of coverage instrumentation for Sanitizers and additional options separated by commas -sanitize=<check> Turn on runtime checks for erroneous behavior. -save-optimization-record-path <value> Specify the file name of any generated YAML optimization record -save-optimization-record Generate a YAML optimization record file -save-temps Save intermediate compilation results -sdk <sdk> Compile against <sdk> -serialize-diagnostics Serialize diagnostics in a binary format -static-executable Statically link the executable -static-stdlib Statically link the Swift standard library -static Make this module statically linkable and make the output of -emit-library a static library. -suppress-warnings Suppress all warnings -swift-version <vers> Interpret input according to a specific Swift language version number -target-cpu <value> Generate code for a particular CPU variant -target-variant <value> Generate code that may run on a particular variant of the deployment target -target <value> Generate code for the given target -tools-directory <directory> Look for external executables (ld, clang, binutils) in <directory> -track-system-dependencies Track system dependencies while emitting Make-style dependencies -use-ld=<value> Specifies the linker to be used -verify-debug-info Verify the binary representation of debug output. -version Print version information and exit -vfsoverlay <value> Add directory to VFS overlay file -v Show commands to run and use verbose output -warn-implicit-overrides Warn about implicit overrides of protocol members -warn-swift3-objc-inference-complete Warn about deprecated @objc inference in Swift 3 for every declaration that will no longer be inferred as @objc in Swift 4 -warn-swift3-objc-inference-minimal Warn about deprecated @objc inference in Swift 3 based on direct uses of the Objective-C entrypoint -warnings-as-errors Treat warnings as errors -whole-module-optimization Optimize input files together instead of individually -working-directory <path> Resolve file paths relative to the specified directory -Xcc <arg> Pass <arg> to the C/C++/Objective-C compiler -Xlinker <value> Specifies an option which should be passed to the linker先写一段简单的 Swift 代码import Foundation print("hello, world")然后用 -dump-ast 导出它的语法树,应该就能看到打印出来的抽象语法树。swiftc -dump-ast main.swift再来看看 Swift 中间代码长啥样,使用 -emit-sil 参数。swiftc -emit-sil main.swift至于其他命令,也是通过类似方式执行就能得到对应的结果。基础语法Constants 及 VariablesSwift 用 let 表示常量, var 表示变量,代码长下面这样。我看官方文档说 let 表示常量,我个人认为 let 表示的是一个变量不可变,当然还是以官方为准比较好。 下面第一行代码,如果 let a 但是并不指定类型也不赋值,就会报错let a: Int a = 11 let b = 12 var c = 11 c = 14编译器会提示这样的错误let a // Type annotation missing in pattern标识符标识符也就是 identifier,譬如下面的 a 跟 test 就是标识符let a = 10 func test() {}Swift 语言的标识符不能使用数字、空白字符(譬如 tab、换行符)、箭头之类的特殊字符,不能用数字开头的这很好理解,因为编译器会很难区分当前读取的一连串字符到底是数字(譬如语言中的整型,浮点型)还是单纯的标识符。不过像下面这种代码是支持的func 🐂🍺() -> Int { return 666 } print(🐂🍺())值类型IntegersSwift 的整型是 Int 表示,也有对应精度的 Int8、Int16、Int32、Int64、UInt8、UInt16、UInt32、UInt64,Int 对应 32 位平台上就会自动使用 Int32,对应 64 位平台上就会使用 Int64,观感上类似 objc 的 NSInteger,如果要写一些早期 8bit 的游戏平台仿真器(譬如 NES)就会用到 UInt8 这个类型,通常在 iOS 开发中直接用 Int 就好。Swift 也可以使用类似下面这样的数值表示let a = 16 // 十进制的 16 let b = 0b1_0000 // 二进制的 16 let c = 0o20 // 八进制的 16 let d = 0x10 // 十六进制的 16Floating-Point NumbersSwift 的浮点型也是可以套用其他语言的认知,分 Float、Double 两种,另外浮点型支持科学计数法let a = 12.0 let b = 1.2e1 // 科学计数法 a == b // true let c = 0x06p1 // 6 * (2 ^ 1) a == c // true let d = 0x06p-1 // 6 * (2 ^ -1) d == 3.0 // true let e = 0x1.1p0 // (1 + 1.0 / 16.0) * (2 ^ 0)Numeric Type ConversionSwift 的数字(譬如 Int16 跟 Int8,Int 跟 Double)之间做运算,需要转成相同的类型。其实这些在其他语言看来是基本类型的东西(譬如 Int)在 Swift 看来是结构体,它们的 + 号也是 Int/Double 结构体上定义的静态方法,然后这个静态方法限定了左右两边相加的类型,所以如果要把一个整型跟一个浮点型相加,那就把其中一个数的值给另一个数相同类型的构造函数构造出实例,于是下面就变成了 Double(a) + blet a = 3 let b = 0.14 // let result = a + b // Binary operator '+' cannot be applied to operands of type 'Int' and 'Double' let result = Double(a) + b // 3.14如果不指定类型,直接把两个 literal(字面量)相加,就不需要写构造let result = 3 + 0.14 // 3.14Tuples苹果官方文档上有下面一段代码,因为我学过 Rust,一看这个东西,就感觉它跟 Rust 用法应该差不多。let http404Error = (404, "Not Found")应该是通过 http404Error.0 、http404Error.1 这种方式访问数据,然后之前用过一段时间 Swift,知道它也有模式匹配,所以肯定也能这样取数据let (code, message) = http404Error因为很多 OCaml 风格的语言(譬如 Rust, ReScript)都用 _ 的符号表示不使用某个值,所以下面这段代码理解当然地从脑海中冒出来。let (code, _) = http404Error当然官方文档还给出了另一种构造跟访问元组的方式let http200Status = (statusCode: 200, description: "OK") print(http200Status.statusCode) print(http200Status.1)StringSwift 的字符串字面量直接用双引号包裹就行,这玩意还支持 + 之类的运算符,还有类似模板字符串的东西,多行字符串也是可以的。Swift 字符串操作太多了,后面再细看一下。var str = "some str" str += " end" str = "\(str)." // "some str end." str = """ line break """Character如果给一个字符串指定 Character 类型,就得到一个字符了,当然当前字符串超出一个字符,就会收到编译器错误let char: Character = "🍺" print(char) // 🍺 let c: Character = "123" // Cannot convert value of type 'String' to specified type 'Character'未完待续
2022年11月23日
80 阅读
0 评论
0 点赞
2021-03-22
0b0010-抽象bit操作
提出问题这趟没屁话。接上文,我们发觉一桩事,修改状态寄存器的时候,我们不够抽象,譬如我想处理 Zero Flag,我需要通过位运算符去操作一个数,每次写二进制,很难受,现在来改得抽象一些解决问题我们发觉状态寄存器有 C、Z、I、D、B、U、V、N 这些状态,那就定义一个类型来描述它们。状态描述状态含义Carry Flag进位标志,操作后导致结果的第 7 位溢出或者 0 位下溢,就设置Zero Flag零标志,如果操作结果为零,就设置。Interrupt Disable中断禁用标志,设置后 CPU 将不处理设备的中断,除非执行清除中断禁用。Decimal Mode十进制模式,CPU 在进行加减时遵守二进制编码的十进制(BCD)算术规则。BBRK 相关U中断相关,这个要配合 B 标志使用Overflow Flag溢出标志,结果出现溢出时设置。Negative Flag负标志,操作结果的第 7 位设置为 1,就要设置。关于中断这里先不讲,直接讲概念没用,后面有实际场景来补充更好理解。struct Flag: OptionSet { static let C = Flag(rawValue: 0b0000_0001) // 1 << 0 static let Z = Flag(rawValue: 0b0000_0010) // 1 << 1 static let I = Flag(rawValue: 0b0000_0100) // 1 << 2 static let D = Flag(rawValue: 0b0000_1000) // 1 << 3 static let B = Flag(rawValue: 0b0001_0000) // 1 << 4 static let U = Flag(rawValue: 0b0010_0000) // 1 << 5 static let V = Flag(rawValue: 0b0100_0000) // 1 << 6 static let N = Flag(rawValue: 0b1000_0000) // 1 << 7 internal var rawValue: UInt8 = 0 init(rawValue flag: UInt8) { rawValue = flag } func bits() -> UInt8 { rawValue } }这样我就把这些状态抽象出来了,如果不想用 rawValue 这个命名,可以把 OptionSet 去掉,反正对应着现在的内容写就可以,使用是一样的效果,OptionSet 是一个协议,用来约束我们的结构体。然后我们要做得就是把行为抽象,根据我们目前已有的代码,我们需要两个函数,当然我也自作主张地添加了一个 set 方法用来处理分支的情况extension Flag { mutating func insert(other: Flag) { rawValue |= other.bits() } mutating func remove(other: Flag) { rawValue &= (~other.bits()) } mutating func set(other: Flag, condition: Bool) { if condition { insert(other) } else { remove(other) } } }修改我们得把 CPU 的 status 改成我们定义的 Flag 结构体class CPU { // ... var status: Flag = Flag(rawValue: 0b0010_0100) // ... } extension CPU { func reset() { // ... status = Flag(rawValue: 0b0010_0100) // ... } }我们的仿真代码也要改一下extension CPU { func interpret(program: [UInt8]) { pc = 0 while true { let code = program[Int(pc)] pc += 1 switch code { // ... case 0xa9: let param = program[Int(pc)] pc += 1 a = param // 处理 Zero Flag status.set(other: .Z, condition: a == 0) // 处理 Negative Flag status.set(other: .N, condition: a >> 7 == 1) default: break } } } }测试由于我们把实现修改嘞,所以我们还要把测试代码改一下,看看效果func testSth() { let cpu = CPU() cpu.interpret(program: [0xa9, 0x00, 0x00]) assert(cpu.status.bits() & 0b0000_0010 == 0b10) cpu.reset() cpu.interpret(program: [0xa9, 0x05, 0x00]) assert(cpu.a == 0x05) assert(cpu.status.bits() & 0b0000_0010 == 0) assert(cpu.status.bits() & 0b1000_0000 == 0) }Good Job,现在只要一两行代码就把之前的操作涵盖了,后面仿真其他指令的时候就更容易嘞,心智负担不会太重。
2021年03月22日
36 阅读
0 评论
0 点赞
2021-03-21
0b0001-开始 emulate CPU
日常讲屁话继续我们的日常水文,这节我们来仿真一个 CPU,其实主要工作就是仿真一个 CPU 的指令。我也看过一些别人的好文讲得是从一个 NES 的 ROM 出发,一步一步仿真 NES,不过我觉得那样不太好讲故事。6502 指令开始仿真 CPU 之前,先祭出这个网站,这个网站把 6502 的指令基本都描述了一下(6502 Reference)通过点击对应的指令,可以锚点到对应的介绍。我们可以看到图上有很多指令,其实 NES 有很多非官方指令,不过不要慌,我们把这些指令仿真先,后续考虑处理非官方的一些指令。描述 CPU现在先写个 CPU 的 class/struct,Swift 的 class 是引用类型,而 struct 是值类型,这里看个人的喜好,我直接用 class 来声明。class CPU { var a: UInt8 = 0 // 累加寄存器 a var x: UInt8 = 0 // 变址寄存器 x var y: UInt8 = 0 // 变址寄存器 y var status: UInt8 = 0 // 状态寄存器 var sp: UInt8 = 0 // 堆栈指针 var pc: UInt16 = 0 // 程序计数器 }寄存器 a 是比较常用的,主要用于读写数据,进行一些逻辑运算寄存器 x 跟 a 差不多,但是它可以更方便 +/- 1,主要用于数据传输、运算等操作寄存器 y 性质跟 x 类似状态寄存器主要保存指令执行时的状态信息,这块有多个状态,分别是 C、Z、I、D、B、V、N,后面写代码会用到堆栈的指针指向一块栈内存这个后面实现的时候展开程序计数器,其实可以理解成一个数组的下标索引,它的寻址范围可以从 0x0000 到 0xFFFF,CPU 根据 pc 找到对应的存储单元,pc 可以自动加 1,CPU 每次读取一条指令,pc 就会自动加 1,当然我们也可以让它跳转返回来改变执行顺序BRK现在把 CPU 做为一个整体,用它来读取“程序”,首先实现一下 BRK 指令,通过上面那个网址查表,我们发现 BRK 是 $00(也就是 0x00,$ 表示 16 进制),它的 Addressing Mode(地址模式) 是 Implied,先不管那么多,直接执行到 BRK 让它跳出循环,如果要真实仿真 BRK 指令,需要把中断的模式都仿真完整,图简单直接 return 吧。extension CPU { func interpret(program: [UInt8]) { pc = 0 while true { let code = program[Int(pc)] pc += 1 switch code { case 0x00: return default: break } } } }目前来看,这好无聊。LDA我们再来把另一个 LDA 实现一下,查表得到 LDA 有多个地址模式,Immediate,Zero Page,Zero Page X,Absolute X……一个一个来,先把快速模式(Immediate)解决掉,这个 Opcode 是 0xa9,此外还要修改 status,这里分别对 Zero Flag 跟 Negative Flag 有影响(Zero Flag 就是上面讲到得 Z,Negative Flag 就是上面讲到的 N)。extension CPU { func interpret(program: [UInt8]) { pc = 0 while true { let code = program[Int(pc)] pc += 1 switch code { // ... case 0xa9: let param = program[Int(pc)] pc += 1 a = param // 处理 Zero Flag if a == 0 { status |= 0b0000_0010 } else { status &= (~0b0000_0010) } // 处理 Negative Flag if a >> 7 == 1 { status |= 0b1000_0000 } else { status &= (~0b1000_0000) } default: break } } } }现在已经把 Immediate 模式的 LDA 处理脱,还有其他 0xa5、0xb5……这些,尝试动手处理一下。测试用例由于我建项目的时候,启用了测试 target,可以写点代码测试一下我们实现的指令,为了方便测试,我们实现一个 reset 函数extension CPU { func reset() { a = 0 x = 0 y = 0 status = 0 sp = 0 pc = 0 } }然后在测试 target 里写上测试的函数,跑一下func testSth() { let cpu = CPU() cpu.interpret(program: [0xa9, 0x00, 0x00]) assert(cpu.status & 0b0000_0010 == 0b10) cpu.reset() cpu.interpret(program: [0xa9, 0x05, 0x00]) assert(cpu.a == 0x05) assert(cpu.status & 0b0000_0010 == 0) assert(cpu.status & 0b1000_0000 == 0) }现在我们已经稍微了解了一挨挨仿真指令的处理方式,后面一节我们继续来优化当前的这些代码。代码会再实现完 CPU 后放到 gayhub 上。
2021年03月21日
51 阅读
0 评论
0 点赞
2021-02-23
0b0000-写个 NES emulator
日常讲屁话吃得饱,打算写个 NES emulator,语言使用 Swift(一边看文档一边学),目标是做个 macOS App,如果有人有想法,把它移植到 iOS 上也可以。macOS App 开发也是临时看文档学,代码质量估计不高,先这样吧。NES:是 FC 主机欧美地区发售的称呼。1985年10月,NES 正式在西方国家推出,除了名称由 Family Computer 改为 Nintendo Entertainment System,造型设计也大幅改变。我们 80/90 后都玩过的小霸王就是内置了一个 NES,比较出名的游戏如超级马里奥,我们的大目标就是可以仿真运行一个超级马里奥初代。设备结构开发一个 NES emulator 之前,我们来过一遍 NES 的主要组成部分。基本上有这些模块CPU NES 的处理器是 MOS 6502 的衍生产品,譬如 Apple II 的处理器就是它这一系的,这是我在网上找到的一篇关于该处理器的文章 MOS Technology 6502 CPU。我读中学的时期,国内有一些低端学习机(譬如电子词典这类)用得也是这个 CPU,具体可以查一下,此处不赘述。它是一个 8 位的处理器,频率是 1.7897725MHz(通常是 NTSC 机型, PAL 机型下频率只有 1.773447MHz),中断模式有 RESET、NMI、IRQ。PPU 这个全称是 Picture Processing Unit,这玩意是用来处理图形的。它会把图像用 256 * 240 的分辨率输出(受限于 NTSC 上下各 8 行显示不了,其实只能显示 256 * 224),调色盘可显示 48 色 5 个灰阶(现在看起来好像蛮弱的),NES 中图像分 Background 跟 Sprite,通过组合两者来显示完整的图像。如果你尝试开发过 Game Boy 的小游戏, 你会发现这两个概念好眼熟,因为受限于那个年代游戏机平台的硬件机能,很多 2D 游戏机是通过类似方式处理图像的(Sprite 在 Web 前端应用或小游戏开发里也有相应的应用)。RAM NES 的 CPU 跟 PPU 都有 2KiB 大小的内存APU 是一个音频处理器,全称 Audio Processing Unit,这个模块其实集成在 CPU 中, 所以它并不是独立的芯片,可以称它为 p(seudo)APU,它提供了五个通道用来处理音频,分别是两个矩形波通道,一个三角波通道, 一个噪声波通道(譬如爆炸声),一个音频采样通道(主要用于处理背景音)。卡带 由于 NES 没有操作系统,所以内容由卡带提供,每个卡带至少包含两个东西,一个是 CHR ROM 主要用于存储游戏图形的数据,另一个是 PRG ROM,主要是用来存储游戏的指令。现实中卡带插入到机器后,CHR ROM 直接连接到 PPU,PRG 连接到 CPU,NES 后续的机器还用卡带提供其他的扩展,不过目前我们先不太深究这块内容手柄 这就是个输入设备就不细讲了其实这些就是我们的目标,我们要把这些模块都用代码描述出来,由于我们用得是高级语言,所以很多东西都变得简单起来了。创建项目现在来启个新项目,具体怎么创建 macOS 项目就不截图描述了,我就不用 SwiftUI 模板创建项目,因为我只想要显示一个窗口,输入相关也是通过键盘处理,界面上也没有太复杂的 UI,然后把 AppDelegate 类改一下import Cocoa @main class AppDelegate: NSObject, NSApplicationDelegate { var window: NSWindow? func applicationDidFinishLaunching(_ aNotification: Notification) { window = NSApplication.shared.windows.first window?.title = "NES emulator" window?.setFrame(CGRect(x: 0, y: 0, width: 960, height: 544), display: true) window?.center() } func applicationWillTerminate(_ aNotification: Notification) { } func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { if !flag { for a in sender.windows { if let w = a as NSWindow? { w.makeKeyAndOrderFront(self) } } } return true } }现在第一步已经处理好,后续水文主要专注写 emulator 的逻辑,再把仿真后的内容展示在窗口上。
2021年02月23日
84 阅读
0 评论
0 点赞