Swiftでファミコンエミュレータを書いた(3)

githubに上げた。

github.com

わかっている問題

  • 音は出ない
  • 遅い
  • iOS実機で動作確認してない
  • 対応している mapper は NROM だけ

遅い問題

Debug build だと 7 fpsくらいしか出ない。 (MacBook 2017, iOS Simulator)

Release build だと 40 fps くらい出るので optimize は非常によく効いている。 逆に言うと optimize に頼りすぎな書き方をしているかもしれない。

iOS 実機では試してないのでどのくらいの性能がでるのかわからない。 iOS 実機を持ってないので。

Instruments で見てみると PPU の rendering がほぼ 90% 以上、までは分かるのだけど どこがボトルネックなのかはよくわからない。 Instruments の見方がよくわからない。

音が出ない問題

APUを作ってないので。 たぶん iOS 実機を手に入れたらつくる、かもしれない。

ふりかえり

  1. CPU は nestest.nes をパスさせるのが大変だった。
  2. PPU は vblank と sprite 0 hit の実装が breakthrough だった

Swift製NESエミュレータその2

遅々として進まず。

nestest はまだたくさん失敗している。

f:id:hrt1ro:20181102195412p:plain

「テスト失敗」の結果を得るまでが大変だった。 実装のミスではなく、仕様を読み違えている。 6502 の CMP のキャリーフラグの仕様変だよね。 思い込みはバグのもと。

color_test はちょっと動いている様子。

f:id:hrt1ro:20181102195431p:plain

palette は画面が崩れまくっている。

f:id:hrt1ro:20181102195444p:plain

まだまだ先は長い。

Swiftでファミコンのエミュレータを書いてみる

先週読んだこのブログエントリ Kotlinでファミコンのエミュレータを書いた - ゆいきノート に刺激を受けたので自分でも書いてみることにした。 Swift4.2, iOS12.0, Xcode10.0で。

なるべく他人の書いたソースコードは読まないようにして、 できるだけ文書や記事だけ読んで実装してみた。 これってプログラムを書く能力よりも 仕様書を読み取る能力のほうが大事だと思った。

ようやくHELLO WORLD!が表示できたので記念にスクリーンショットを貼っておく。

macOS上のiPhoneシミュレータ上で動いているNESエミュレータの図

f:id:hrt1ro:20181025174428p:plain

ソースはそのうちGitHubあたりに公開するかもしれない。 だいぶ飽きてきたのでいつになるかわからない。

Core Graphics で半透明図形を描く

図形を塗りつぶす。

        UIColor.red.setFill()
        UIBezierPath(rect: CGRect(x: 80, y: 80, width: 100, height: 100)).fill()

重ねて描く。

        UIColor.blue.setFill()
        UIBezierPath(rect: CGRect(x: 100, y: 100, width: 100, height: 100)).fill()

ここから半透明で図形を描く。

まず現在の描画文脈 CGContext インスタンスを得る。

        let context = UIGraphicsGetCurrentContext()!

alpha値を変更するので後でもとに戻す。

        context.saveGState()
        defer { context.restoreGState() }

alpha値を指定する。

        context.setAlpha(0.5)

不透明のときと同じように重ねて描く。

        UIColor.yellow.setFill()
        UIBezierPath(rect: CGRect(x: 120, y: 40, width: 100, height: 100)).fill()

        UIColor.cyan.setFill()
        UIBezierPath(rect: CGRect(x: 140, y: 60, width: 100, height: 100)).fill()

半透明の図形が重なる。

f:id:hrt1ro:20181009112424p:plain

さて「半透明図形を重ねる」のではなく、 「不透明図形を重ねたやつを半透明で重ねたい」という要件がある、 ということもあるかもしれない。

CGContext.beginTransparencyLayer(auxiliaryInfo:)APIドキュメントによるとこう書いてある。

The global alpha is set to 1.

setAlpha() の後、TransparencyLayerを beginend で囲めば、 不透明で重ねた図形を 半透明で描くことができる。

        context.beginTransparencyLayer(in: CGRect(x: 120.0, y: 40.0, width: 120.0, height: 120.0), auxiliaryInfo: nil)
        defer { context.endTransparencyLayer() }

こんなかんじ

f:id:hrt1ro:20181009112442p:plain

beginendはネストできるので、beginendの間で setAlpha() すればネストした内と外の alpha値を乗算した半透明で描かれることになる。

Core Text で縦書き(2)

NSAttributedString.draw(in:) なら Core Text 無しで縦書きできんじゃね? と思って試してみたけどそんな単純じゃなかった。 役物だけ回転しているので近づこうとしている気配はあるけど、 なにか指定方法があるのかどうなのか、ちょっとわからなかった。

さて Core Text をつかうほうだけど、 前回ASCII文字を混ぜたらどうも行間が広くなってしまうと書いたけど、 パラグラフスタイルの行の高さをフォントの高さくらいで指定してやると良いようだ。 ついでにフォントも指定してやる。

    let font = UIFont.systemFont(ofSize: 14)
    let paraStyle = NSMutableParagraphStyle()
    paraStyle.maximumLineHeight = font.lineHeight
    let astr = NSAttributedString(string: string, attributes: [
      .font: font,
      .verticalGlyphForm: true,
      .paragraphStyle: paraStyle,
    ])

f:id:hrt1ro:20180930142123p:plain

ここでふと気づいたのだけど、ASCIIの文字も縦になるのはいつからなのだろう。 確か昔のiOSだと横に倒れたままだったと記憶しているのだけど…昔過ぎて忘れた。

ASCIIの文字は倒しておきたいので アトリビュート.verticalGlyphForm: false にしておきたい。 ということは文字単位で判定してアトリビュートを切り替える? 縦書きにしたいものはとりあえず全部全角にしておく?

役物の対応を見ていて、全角コロン(U+FF1A)、三点リーダ(U+2026)、全角ダッシュ (U+2014)が縦にならないのは、 これは仕様なのか未対応なのか何かパラメータがあるのか、ちょっとわからなかった。

世の中ちょっとわからないことが多い。

Core Text で縦書き

Core Text でテキストを描画するには CTFrameDraw(_:_:) を呼べばいいのだけど、そのパラメータをどうつくるかという話。

FrameDraw 第2引数の CGContextUIGraphicsGetCurrentContext() で得られる。

FrameDraw 第1引数の CTFrameCTFramesetterCreateFrame(_:_:_:_:) で作れる。

CreateFrame 第3引数 の CGPathUIBezierPath から作れるけど、 CGRect からなら CGPath(rect:transform:) のほうが早い。

CreateFrame 第1引数の CTFramesetterCTFramesetterCreateWithAttributedString(_:) で作れる。

引数の CFAttributedStringAPI ドキュメントによると

CFAttributedString is “toll-free bridged” with its Foundation counterpart, NSAttributedString.

とのことなので NSAttributedString を使えば良いようである。

let astr = NSAttributedString(string: string) // アトリビュートなし

アトリビュートに縦書きを指定するには、

let astr = NSAttributedString(string: string, attributes: [.verticalGlyphForm: true])

CTFrame を作るところだけまとめると、

func createFrame(string: String, rect: CGRect) -> CTFrame? {
    let astr = NSAttributedString(string: string, attributes: [.verticalGlyphForm: true])
    let setter = CTFramesetterCreateWithAttributedString(astr)
    let path = CGPath(rect: rect, transform: nil)
    return CTFramesetterCreateFrame(setter, CFRange(), path, nil)
}

描画はこうなる。

let frame = createFrame(string: string, rect: rect)!
let context = UIGraphicsGetCurrentContext()!
CTFrameDraw(frame, context)

赤い枠線は rect を描画したもの。

f:id:hrt1ro:20180927132425p:plain

文字が裏返っている理由は、 Core Textは LLO(Lower Left Origin)の座標系で描くのだけど、 UIKit 由来の CGContext は ULO (Upper Left Origin) の座標系だから。 LLOで描いたものがULOに正しく描画されるように、座標変換させる必要がある。

座標変換するときは、行儀よく保存・復帰させよう。

context.saveGState()
defer { context.restoreGState() }

LLO→ULO座標変換はy軸を反転させるのがふつうなので

context.scaleBy(x: 1, y: -1) // 上下反転(LLO→ULO)

変換後の座標系での rect を点線で描いてある。

f:id:hrt1ro:20180927132459p:plain

裏返るのは直ったけど、縦書きの向きにしたいので90度回転させる。

context.rotate(by: CGFloat.pi/2) // 90度回転
context.scaleBy(x: 1, y: -1) // 上下反転(LLO→ULO)

順序はこの通りでないといけない。

f:id:hrt1ro:20180927132521p:plain

90度回転させると枠の縦横が入れ替わってしまうので、rect を LLO の座標系に逆変換してやらねばなるまい。

まず、一度 Affine 変換をつくってCTMに結合するやりかたに変更する。

let transform = CGAffineTransform(rotationAngle: CGFloat.pi/2).scaledBy(x: 1, y: -1)
context.concatenate(transform)

逆変換したrectで描くと、正位置に出るはず。

let frame = createFrame(string: string, rect: rect.applying(transform.inverted()))!

f:id:hrt1ro:20180927132542p:plain

さて調子に乗って NSString.draw(at:withAttributes:) と混ぜると大変なことになる

f:id:hrt1ro:20180927132556p:plain

これは NSString.draw(at:withAttributes:)CGContext.textMatrix を変更するのだけど、元に戻してくれないため。 そして CTFrameDraw(_:_:)CGContext.textMatrix に従って描画するため、だから。

しかたがないので自分で元に戻してやる。というかそもそもそういうもののようである。

context.textMatrix = CGAffineTransform.identity

まとめるとこんなかんじ。

func drawVText(context: CGContext, string: String, rect: CGRect) {
    context.saveGState()
    defer { context.restoreGState() }
    let transform = CGAffineTransform(rotationAngle: CGFloat.pi/2).scaledBy(x: 1, y: -1)
    context.concatenate(transform)
    let frame = createFrame(string: string, rect: rect.applying(transform.inverted()))!
    context.textMatrix = CGAffineTransform.identity
    CTFrameDraw(frame, context)
}
func createFrame(string: String, rect: CGRect) -> CTFrame? {
    let astr = NSAttributedString(string: string, attributes: [.verticalGlyphForm: true])
    let setter = CTFramesetterCreateWithAttributedString(astr)
    let path = CGPath(rect: rect, transform: nil)
    return CTFramesetterCreateFrame(setter, CFRange(), path, nil)
}

さてASCIIな文字を混ぜたりするとちょっと期待と異なるので、調整してやらねばならないのだけれども、 たぶん次回に続く

NSString の UIKit 拡張で文字列を描くやり方

iOSUIView 派生クラスで func draw(_:)override して UIBezierPath やら UIImage やらを描画する横並びで、 文字列も描画するやつの話。

import UIKit すると NSString.draw(at:withAttributes:)NSString.draw(in:withAttributes:) が使える。

NSStringString から as で型変換できる

let string = "Hello"
let nsstring = string as NSString

(10, 10)の位置に文字列を描く(アトリビュートなし)。

let p = CGPoint(x: 10, y: 10)
nsstring.draw(at: p)

ついでに枠線も描いてみる。 NSString.size(withAttributes:) で文字列を描くために必要な領域の高さと幅が得られる。

let size = nsstring.size()
UIColor.red.setStroke()
UIBezierPath(rect: CGRect(origin: p, size: size)).stroke()

f:id:hrt1ro:20180925095849p:plain

ところでなぜだか NSString ではなく Stringインスタンスでも NSString.draw(at:withAttributes:) が呼び出せる様子。

let string = "Hello"
let p = CGPoint(x: 10, y: 10)
string.draw(at: p)  // ←これ
let size = string.size() // ←これも
UIColor.red.setStroke()
UIBezierPath(rect: CGRect(origin: p, size: size)).stroke()

これは、暗黙の型変換がされているのか、何か特別な対応がされているのか、 小一時間調べたくらいじゃわからなかった。 (教えて偉い人)