├── .dockerignore ├── .gitignore ├── .rubocop.yml ├── .travis.yml ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── benchmark.yml ├── bin ├── optcarrot ├── optcarrot-bench ├── optcarrot-bench-parallel-on-ractor └── optcarrot-bench3000 ├── doc ├── benchmark-full-3000.png ├── benchmark-full.png ├── benchmark-summary-3000.png ├── benchmark-summary.png ├── benchmark.md ├── bonus.md └── internal.md ├── examples ├── DABG.zip ├── Lan Master.zip ├── Lan_Master.nes ├── alter_ego.zip ├── lawn_mower.zip ├── source.yml ├── thwaite-0-03.zip └── zooming_secretary1-02.zip ├── lib ├── optcarrot.rb └── optcarrot │ ├── apu.rb │ ├── config.rb │ ├── cpu.rb │ ├── driver.rb │ ├── driver │ ├── ao_audio.rb │ ├── gif_video.rb │ ├── log_input.rb │ ├── misc.rb │ ├── mplayer_video.rb │ ├── png_video.rb │ ├── sdl2.rb │ ├── sdl2_audio.rb │ ├── sdl2_input.rb │ ├── sdl2_video.rb │ ├── sfml.rb │ ├── sfml_audio.rb │ ├── sfml_input.rb │ ├── sfml_video.rb │ ├── sixel_video.rb │ ├── term_input.rb │ └── wav_audio.rb │ ├── mapper │ ├── cnrom.rb │ ├── mmc1.rb │ ├── mmc3.rb │ └── uxrom.rb │ ├── nes.rb │ ├── opt.rb │ ├── pad.rb │ ├── palette.rb │ ├── ppu.rb │ └── rom.rb ├── optcarrot.gemspec └── tools ├── README ├── chart-images.js ├── compile-ico.rb ├── list-method-calls.rb ├── mruby_optcarrot_config.rb ├── plot.rb ├── reader.rb ├── rewrite.rb ├── run-benchmark.rb ├── run-tests.rb ├── shim.rb └── statistic-test.rb /.dockerignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | Gemfile.lock 3 | coverage/ 4 | doc/ 5 | pkg/ 6 | vendor/ 7 | optcarrot-*.gem 8 | 9 | .git 10 | .dockerignore 11 | .*.sw* 12 | **/.*.sw* 13 | tools/nes-test-roms 14 | SDL2.dll 15 | video.png 16 | video.gif 17 | audio.wav 18 | stackprof-*.dump 19 | perf.data 20 | perf.data.old 21 | benchmark/bm-*.csv 22 | benchmark/Dockerfile.* 23 | tmp 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /Gemfile.lock 3 | /benchmark 4 | /coverage/ 5 | /pkg/ 6 | /vendor/ 7 | optcarrot-*.gem 8 | 9 | .*.sw* 10 | /tools/nes-test-roms 11 | video.png 12 | video.gif 13 | audio.wav 14 | stackprof-*.dump 15 | perf.data 16 | perf.data.old 17 | benchmark/bm-*.csv 18 | ppu-core.rb 19 | cpu-core.rb 20 | benchmark/Dockerfile.* 21 | benchmark/*-core-opt-*.rb 22 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | NewCops: enable 3 | Exclude: 4 | - "*-core.rb" 5 | - "tools/plot.rb" 6 | - "tmp/**" 7 | - "test/**" 8 | - "benchmark/*-core-opt-*.rb" 9 | - "benchmark/Dockerfile.*" 10 | 11 | # "eval" is the swiss army knife 12 | Security/Eval: 13 | Enabled: false 14 | 15 | # Marshal.load is needed when needed 16 | Security/MarshalLoad: 17 | Enabled: false 18 | 19 | # "while true" loop is easy and fast 20 | Lint/Loop: 21 | Enabled: false 22 | Style/InfiniteLoop: 23 | Enabled: false 24 | 25 | # `String#%` is a great invention of Ruby 26 | Style/FormatString: 27 | EnforcedStyle: percent 28 | 29 | # I hate frozen string literal 30 | Style/FrozenStringLiteralComment: 31 | Enabled: false 32 | 33 | # 10_000 looks dirty to me 34 | Style/NumericLiterals: 35 | MinDigits: 6 36 | 37 | # explicit return is sometimes good for consistency 38 | Style/RedundantReturn: 39 | Enabled: false 40 | 41 | # `x == :error ? error-case : normal-case` does not look beautiful to me 42 | Style/NegatedIfElseCondition: 43 | Enabled: false 44 | 45 | # I like `foo {|x| bar(x) }` and `foo { bar }` 46 | Layout/SpaceInsideBlockBraces: 47 | EnforcedStyleForEmptyBraces: space 48 | SpaceBeforeBlockParameters: false 49 | 50 | # `"foo#{bar}baz"` looks too dense to me 51 | Layout/SpaceInsideStringInterpolation: 52 | EnforcedStyle: space 53 | 54 | # I consistently use double quotes 55 | Style/StringLiterals: 56 | EnforcedStyle: double_quotes 57 | Style/StringLiteralsInInterpolation: 58 | EnforcedStyle: double_quotes 59 | 60 | # A trailing comma in array/hash literal is super cool 61 | Style/TrailingCommaInArrayLiteral: 62 | Enabled: false 63 | Style/TrailingCommaInHashLiteral: 64 | Enabled: false 65 | 66 | # I don't like this so much but virtually needed for ffi struct layout 67 | Style/TrailingCommaInArguments: 68 | Enabled: false 69 | 70 | # I don't want to fill my code with `.freeze` 71 | Style/MutableConstant: 72 | Enabled: false 73 | 74 | # I have no idea why this is prohibited... 75 | Style/ParallelAssignment: 76 | Enabled: false 77 | 78 | # Backrefs are indeed dirty, but `Regexp.last_match` is too verbose 79 | Style/PerlBackrefs: 80 | Enabled: false 81 | 82 | # I think `{|ary| ary.size }` is not so bad since its type is explicit 83 | Style/SymbolProc: 84 | Enabled: false 85 | 86 | # Wrapping a code is so bad? Case-by-case. 87 | Style/GuardClause: 88 | Enabled: false 89 | 90 | # I use hyphen-separated case for script program. 91 | Naming/FileName: 92 | Exclude: 93 | - 'tools/*.rb' 94 | 95 | # I don't care class/module size 96 | Metrics/ClassLength: 97 | Max: 1000 98 | Metrics/ModuleLength: 99 | Max: 1000 100 | 101 | # I accept two-screen size (about 100 lines?) 102 | Metrics/MethodLength: 103 | Max: 100 104 | Metrics/BlockLength: 105 | Max: 100 106 | 107 | # Don't worry, my terminal is big enough 108 | Layout/LineLength: 109 | Max: 120 110 | 111 | # Code metrics is good, but I think the default is too strict... 112 | Metrics/CyclomaticComplexity: 113 | Max: 40 114 | Metrics/PerceivedComplexity: 115 | Max: 40 116 | Metrics/AbcSize: 117 | Max: 100 118 | Metrics/BlockNesting: 119 | Max: 5 120 | 121 | # I like `x == 0` 122 | Style/NumericPredicate: 123 | EnforcedStyle: comparison 124 | 125 | # I like `foo1` and `foo_bar_1` 126 | Naming/VariableNumber: 127 | Enabled: false 128 | 129 | # empty is empty 130 | Style/EmptyMethod: 131 | Enabled: false 132 | Lint/EmptyWhen: 133 | Enabled: false 134 | 135 | # if-elsif-elsif... looks awkward to me 136 | Style/EmptyCaseCondition: 137 | Enabled: false 138 | 139 | # `rescue StandardError` looks redundant to me 140 | Style/RescueStandardError: 141 | Enabled: false 142 | 143 | # `END` is always my heredoc delimiter 144 | Naming/HeredocDelimiterNaming: 145 | Enabled: false 146 | 147 | # I like `%w()` 148 | Style/PercentLiteralDelimiters: 149 | PreferredDelimiters: 150 | '%w': '()' 151 | 152 | # I cannot use `%i()` since I want to make this code run in 1.8 153 | Style/SymbolArray: 154 | EnforcedStyle: brackets 155 | 156 | # `0 <= n && n <= 0x7f` is completely innocent 157 | Style/YodaCondition: 158 | Enabled: false 159 | 160 | # I understand but `while true` is faster than `loop do` 161 | Lint/LiteralAsCondition: 162 | Enabled: false 163 | 164 | # What I want to do is to puts a message to stderr, not to warn users 165 | Style/StderrPuts: 166 | Exclude: 167 | - 'tools/shim.rb' 168 | 169 | # Leave me alone 170 | Style/CommentedKeyword: 171 | Enabled: false 172 | 173 | # I want to casually use %s for simple format 174 | Style/FormatStringToken: 175 | Enabled: false 176 | 177 | # Indeed, if having a single-line body is not so smart, but just not smart 178 | Style/IfUnlessModifier: 179 | Enabled: false 180 | 181 | # Let me choose "" + "" 182 | Style/StringConcatenation: 183 | Enabled: false 184 | 185 | # Keyword arguments cannot be used in Ruby 1.8 186 | Style/OptionalBooleanParameter: 187 | Enabled: false 188 | 189 | # I don't use `Kernel#Array` 190 | Style/ArrayCoercion: 191 | Enabled: false 192 | 193 | # `(1 + 1)**-1` looks awkward 194 | Layout/SpaceAroundOperators: 195 | Enabled: false 196 | 197 | # One-letter variable is cute 198 | Naming/MethodParameterName: 199 | Enabled: false 200 | 201 | # It highly depends on the context 202 | Layout/EmptyLineAfterGuardClause: 203 | Enabled: false 204 | 205 | # Hash table literal is a kind of an art, difficult for machine 206 | Layout/HashAlignment: 207 | Enabled: false 208 | 209 | # The program needs to work on old rubies 210 | Style/SafeNavigation: 211 | Enabled: false 212 | 213 | # I want to use %w() only when the length is long 214 | Style/WordArray: 215 | Enabled: false 216 | 217 | # I want to align lines 218 | Layout/SpaceAroundMethodCallOperator: 219 | Enabled: false 220 | 221 | # shim is shim 222 | Layout/EmptyLinesAroundAttributeAccessor: 223 | Exclude: 224 | - 'tools/shim.rb' 225 | 226 | # This is sometimes a good habit 227 | Style/RedundantAssignment: 228 | Enabled: false 229 | 230 | # We support Ruby 1.8 231 | Gemspec/RequiredRubyVersion: 232 | Enabled: false 233 | 234 | # Ruby 1.8 does not allow rescue clause in a block 235 | Style/RedundantBegin: 236 | Enabled: false 237 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.7.1 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "benchmark_driver", ">= 0.11.0", group: :development 4 | gem "ffi" 5 | gem "rake", group: [:development, :test] 6 | gem "rubocop", group: :development 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Yusuke Endoh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Optcarrot: A NES Emulator for Ruby Benchmark 2 | 3 | ## Project Goals 4 | 5 | This project aims to provide an "enjoyable" benchmark for Ruby implementation to drive ["Ruby3x3: Ruby 3 will be 3 times faster"][ruby3x3]. 6 | 7 | The specific target is a NES (Nintendo Entertainment System) emulator that works at *20 fps* in Ruby 2.0. An original NES works at 60 fps. If Ruby3x3 is succeeded, we can enjoy a NES game with Ruby! 8 | 9 | NOTE: We do *not* aim to create a practical NES emulator. There have been already many great emulators available. We recommend you use another emulator if you just want to play a game. 10 | 11 | ## Basic usage 12 | 13 | SDL2 is required. 14 | 15 | $ git clone http://github.com/mame/optcarrot.git 16 | $ cd optcarrot 17 | $ bin/optcarrot examples/Lan_Master.nes 18 | 19 | |key |button | 20 | |------|-------------| 21 | |arrow |D-pad | 22 | |`Z` |A button | 23 | |`X` |B button | 24 | |space |Start button | 25 | |return|Select button| 26 | 27 | See [`doc/bonus.md`](doc/bonus.md) for advanced usage. 28 | 29 | ## Benchmark example 30 | 31 | Here is FPS after 3 seconds in the game's clock. 32 | 33 | ![benchmark chart](doc/benchmark-summary.png) 34 | 35 | Here is FPS after 50 seconds in the game's clock. (Only fast implementations are listed.) 36 | 37 | ![benchmark chart for 3000 frames](doc/benchmark-summary-3000.png) 38 | 39 | See [`doc/benchmark.md`](doc/benchmark.md) for the measurement condition and some other charts. 40 | 41 | See also [Ruby Releases Benchmarks](https://rubybench.org/ruby/ruby/releases?result_type=Optcarrot%20Lan_Master.nes) and [Ruby Commits Benchmarks](https://rubybench.org/ruby/ruby/commits?result_type=Optcarrot%20Lan_Master.nes&display_count=2000) for the continuous benchmark results. 42 | 43 | You may also want to read [@eregon's great post](https://eregon.me/blog/2016/11/28/optcarrot.html) for TruffleRuby potential performance after warm-up. 44 | 45 | ## Optimized mode 46 | 47 | It may run faster with the option `--opt`. 48 | 49 | $ bin/optcarrot --opt examples/Lan_Master.nes 50 | 51 | This option will generate an optimized (and super-dirty) Ruby code internally, and replace some bottleneck methods with them. See [`doc/internal.md`](doc/internal.md) in detail. 52 | 53 | ## See also 54 | 55 | * [Slide deck](http://www.slideshare.net/mametter/optcarrot-a-pureruby-nes-emulator) ([Tokyo RubyKaigi 11](http://regional.rubykaigi.org/tokyo11/en/)) 56 | 57 | ## Acknowledgement 58 | 59 | We appreciate all the people who devoted efforts to NES analysis. If it had not been not for the [NESdev Wiki][nesdev-wiki], we could not create this program. We also read the source code of Nestopia, NESICIDE, and others. We used the test ROMs due to NESICIDE. 60 | 61 | [ruby3x3]: https://www.youtube.com/watch?v=LE0g2TUsJ4U&t=3248 62 | [nesdev-wiki]: http://wiki.nesdev.com/w/index.php/NES_reference_guide 63 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | task :test do 2 | ruby "tools/run-tests.rb" 3 | end 4 | 5 | task :benchmark do 6 | ruby "tools/run-benchmark.rb", "all", "-m", "all", "-c", "10" 7 | end 8 | 9 | task :wc do 10 | puts "lines of minimal source code:" 11 | sh "wc -l bin/optcarrot lib/optcarrot.rb lib/optcarrot/*.rb" 12 | end 13 | 14 | task :"wc-all" do 15 | sh "wc -l bin/optcarrot lib/optcarrot.rb lib/optcarrot/*.rb lib/optcarrot/*/*.rb" 16 | end 17 | 18 | task default: :test 19 | -------------------------------------------------------------------------------- /benchmark.yml: -------------------------------------------------------------------------------- 1 | type: ruby_stdout 2 | name: Optcarrot Lan_Master.nes 3 | command: -r./tools/shim.rb bin/optcarrot --benchmark examples/Lan_Master.nes 4 | metrics: 5 | Number of frames: 6 | unit: fps 7 | from_stdout: 'Float(stdout.match(/^fps: (?\d+\.\d+)$/)[:fps])' 8 | environment: 9 | Checksum: 10 | from_stdout: 'Integer(stdout.match(/^checksum: (?\d+)$/)[:checksum])' 11 | -------------------------------------------------------------------------------- /bin/optcarrot: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # I'm too lazy to type `-Ilib` every time... 4 | require_relative "../lib/optcarrot" 5 | 6 | Optcarrot::NES.new.run 7 | -------------------------------------------------------------------------------- /bin/optcarrot-bench: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # I'm too lazy to type `-Ilib` every time... 4 | require_relative "../lib/optcarrot" 5 | 6 | Ractor.new { nil } if ENV["OPTCARROT_DUMMY_RACTOR"] 7 | 8 | argv = ["-b", "--no-print-video-checksum", File.join(__dir__, "../examples/Lan_Master.nes")] + ARGV 9 | Optcarrot::NES.new(argv).run 10 | -------------------------------------------------------------------------------- /bin/optcarrot-bench-parallel-on-ractor: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # How to run: 4 | # 5 | # /path/to/ractor/branch/ruby bin/optcarrot-bench-parallel-on-ractor 6 | # OPTCARROT_RACTOR_PARALLEL=2 /path/to/ractor/branch/ruby bin/optcarrot-bench-parallel-on-ractor 7 | 8 | # I'm too lazy to type `-Ilib` every time... 9 | require_relative "../lib/optcarrot" 10 | 11 | # deep freeze all the constants 12 | 13 | # rubocop:disable Lint/ShadowingOuterLocalVariable, Style/Semicolon 14 | Optcarrot::Config::DEFAULT_OPTIONS.each {|k, v| k.freeze; v.freeze }.freeze 15 | Optcarrot::Config::OPTIONS.each do |k, v| 16 | k.freeze 17 | v.each do |k, v| 18 | k.freeze 19 | v.each do |k, v| 20 | k.freeze 21 | if v.is_a?(Array) 22 | v.each {|v| v.freeze } 23 | end 24 | v.freeze 25 | end.freeze 26 | end.freeze 27 | end.freeze 28 | Optcarrot::Driver::DRIVER_DB.each do |k, v| 29 | k.freeze 30 | v.each {|k, v| k.freeze; v.freeze }.freeze 31 | end.freeze 32 | Optcarrot::Audio::PACK_FORMAT.each {|k, v| k.freeze; v.freeze }.freeze 33 | Optcarrot::APU::Pulse::WAVE_FORM.each {|a| a.freeze }.freeze 34 | Optcarrot::APU::Triangle::WAVE_FORM.freeze 35 | Optcarrot::APU::FRAME_CLOCKS.freeze 36 | Optcarrot::APU::OSCILLATOR_CLOCKS.each {|a| a.freeze }.freeze 37 | Optcarrot::APU::LengthCounter::LUT.freeze 38 | Optcarrot::APU::Noise::LUT.freeze 39 | Optcarrot::APU::Noise::NEXT_BITS_1.each {|a| a.freeze }.freeze 40 | Optcarrot::APU::Noise::NEXT_BITS_6.each {|a| a.freeze }.freeze 41 | Optcarrot::APU::DMC::LUT.freeze 42 | Optcarrot::PPU::DUMMY_FRAME.freeze 43 | Optcarrot::PPU::BOOT_FRAME.freeze 44 | Optcarrot::PPU::SP_PIXEL_POSITIONS.each {|k, v| k.freeze; v.freeze }.freeze 45 | Optcarrot::PPU::TILE_LUT.each {|a| a.each {|a| a.each {|a| a.freeze }.freeze }.freeze }.freeze 46 | Optcarrot::PPU::NMT_TABLE.each {|k, v| k.freeze; v.freeze }.freeze 47 | Optcarrot::CPU::DISPATCH.each {|a| a.freeze }.freeze 48 | Optcarrot::ROM::MAPPER_DB.freeze 49 | # rubocop:enable Style/Semicolon 50 | 51 | # rubocop:disable Style/MultilineBlockChain 52 | argv = ["-b", "--no-print-video-checksum", File.join(__dir__, "../examples/Lan_Master.nes")] + ARGV 53 | (1..(ENV["OPTCARROT_RACTOR_PARALLEL"] || "1").to_i).map do 54 | Ractor.new(argv) do |argv| 55 | Optcarrot::NES.new(argv).run 56 | end 57 | end.each {|r| r.take } 58 | # rubocop:enable Lint/ShadowingOuterLocalVariable, Style/MultilineBlockChain 59 | -------------------------------------------------------------------------------- /bin/optcarrot-bench3000: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # I'm too lazy to type `-Ilib` every time... 4 | require_relative "../lib/optcarrot" 5 | 6 | Ractor.new { nil } if ENV["OPTCARROT_DUMMY_RACTOR"] 7 | 8 | argv = ["-b", "--no-print-video-checksum", "--frames", "3000", File.join(__dir__, "../examples/Lan_Master.nes")] + ARGV 9 | Optcarrot::NES.new(argv).run 10 | -------------------------------------------------------------------------------- /doc/benchmark-full-3000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mame/optcarrot/48639d175fce42b6330e47f7ebbe691938752cef/doc/benchmark-full-3000.png -------------------------------------------------------------------------------- /doc/benchmark-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mame/optcarrot/48639d175fce42b6330e47f7ebbe691938752cef/doc/benchmark-full.png -------------------------------------------------------------------------------- /doc/benchmark-summary-3000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mame/optcarrot/48639d175fce42b6330e47f7ebbe691938752cef/doc/benchmark-summary-3000.png -------------------------------------------------------------------------------- /doc/benchmark-summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mame/optcarrot/48639d175fce42b6330e47f7ebbe691938752cef/doc/benchmark-summary.png -------------------------------------------------------------------------------- /doc/benchmark.md: -------------------------------------------------------------------------------- 1 | # Ruby implementation benchmark with Optcarrot 2 | 3 | ![benchmark chart](benchmark-full.png) 4 | ![benchmark chart for 3000 frames](benchmark-full-3000.png) 5 | 6 | ## Experimental conditions 7 | 8 | * Core i7 4500U (1.80GHz) / Ubuntu 18.04 9 | * Command: `ruby -v -Ilib -r./tools/shim bin/optcarrot --benchmark examples/Lan_Master.nes` 10 | * This runs the first 180 frames (three seconds), and prints the fps of the last ten frames. 11 | * `--benchmark` mode implies no GUI, so GUI overhead is not included. 12 | * [`tools/shim.rb`](../tools/shim.rb) is required for incompatibility of Ruby implementations. 13 | * `--opt` option is added for the optimized mode. 14 | * Furthermore, [`tools/rewrite.rb`](../tools/rewrite.rb) is used for some implementations (currently, Ruby 1.8 and Opal) to work with syntax incompatibility. See [`tools/run-benchmark.rb`](../tools/run-benchmark.rb) in detail. 15 | * Measured fps 10 times for each, and calculated the average over the runs. 16 | * The error bars represent the standard deviation. 17 | 18 | ## Ruby implementations 19 | * master: `ruby 3.1.0dev (2021-11-18T17:47:40Z master 75ecbda438) [x86_64-linux]` 20 | * ruby30: `ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-linux]` 21 | * ruby27: `ruby 2.7.2p137 (2020-10-01 revision 5445e04352) [x86_64-linux]` 22 | * ruby26: `ruby 2.6.6p146 (2020-03-31 revision 67876) [x86_64-linux]` 23 | * ruby25: `ruby 2.5.8p224 (2020-03-31 revision 67882) [x86_64-linux]` 24 | * ruby24: `ruby 2.4.10p364 (2020-03-31 revision 67879) [x86_64-linux]` 25 | * ruby23: `ruby 2.3.8p459 (2018-10-18 revision 65136) [x86_64-linux]` 26 | * ruby22: `ruby 2.2.10p489 (2018-03-28 revision 63023) [x86_64-linux]` 27 | * ruby21: `ruby 2.1.10p492 (2016-04-01 revision 54464) [x86_64-linux]` 28 | * ruby20: `ruby 2.0.0p648 (2015-12-16 revision 53162) [x86_64-linux]` 29 | * ruby193: `ruby 1.9.3p551 (2014-11-13 revision 48407) [x86_64-linux]` 30 | * ruby187: `ruby 1.8.7 (2013-06-27 patchlevel 374) [x86_64-linux]` 31 | 32 | * mastermjit, ruby\*mjit: ruby with `--jit` 33 | * masteryjit, ruby\*yjit: ruby with `--yjit` 34 | 35 | * truffleruby: `truffleruby 20.1.0, like ruby 2.6.5, GraalVM CE JVM [x86_64-linux]` 36 | * jruby: `jruby 9.2.14.0 (2.5.7) 2020-12-08 ebe64bafb9 OpenJDK 64-Bit Server VM 25.275-b01 on 1.8.0_275-b01 +indy +jit [linux-x86_64]` 37 | * `--server -Xcompile.invokedynamic=true` is specified. 38 | 39 | * rubinius: `rubinius 3.107 (2.3.1 387c4887 2018-07-15 5.0.0git-929163d) [x86_64-linux-gnu]` 40 | 41 | * mruby: `mruby 3.0.0preview (2020-10-16)` 42 | * Configured with `MRB_WITHOUT_FLOAT` option 43 | 44 | * topaz: `topaz (ruby-2.4.0p0) (git rev 9287c22) [x86_64-linux]` 45 | * Failed to run the optimized mode maybe because the generated core is so large. 46 | 47 | * opal: `Opal v1.0.5` 48 | * Failed to run the default mode because of lack of Fiber. 49 | 50 | * ruruby: `9c3084b951b3ff9af48feb5c87881760fe3352e1` 51 | 52 | See [`tools/run-benchmark.rb`](../tools/run-benchmark.rb) for the actual commands. 53 | 54 | ## Remarks 55 | 56 | This benchmark may not be fair inherently. Optcarrot is somewhat tuned for MRI since I developed it with MRI. 57 | 58 | The optimized mode assumes that case statement is implemented with "jump table" if all `when` clauses have trivial immediate values such as Integer. This is true for MRI, but it is known that [JRuby 9k](https://github.com/jruby/jruby/issues/3672) and [Rubinius](https://github.com/rubinius/rubinius-code/issues/2) are not (yet). OMR preview also seems not to support JIT for `opt_case_dispatch` instruction. 59 | 60 | ## Hints for Ruby implementation developers 61 | 62 | * This program is purely CPU-intensive. Any improvement of I/O and GC will not help. 63 | 64 | * As said in remarks, this program assumes that the implementation will optimize `case` statements by "jump-table". Checking each clauses in order will be too slow. 65 | * Implementation note: In the optimized mode (`--opt` option), CPU/PPU evaluators consist of one loop with a big `case` statement dispatching upon the current opcode or clock. 66 | 67 | * The hotspot is `PPU#run` and `CPU#run`. The optimized mode replaces them with an automatically generated and optimized source code by using `eval`. 68 | * You can see the generated code with `--dump-cpu` and `--dump-ppu`. See also [`doc/internal.md`](internal.md). 69 | 70 | * The hotspot uses no reflection-like features except `send` and `Method#[]`. 71 | * Implementation note: CPU dispatching uses `send` in the default mode. Memory-mapped I/O is implemented by exploiting polymorphism of `Method#[]` and `Array#[]`. 72 | 73 | * If you are a MRI developer, you can reduce compile time by using `miniruby`. 74 | 75 | ~~~~ 76 | $ git clone https://github.com/ruby/ruby.git 77 | $ cd ruby 78 | $ ./configure 79 | $ make miniruby -j 4 80 | $ ./miniruby /path/to/optcarrot --benchmark /path/to/Lan_Master.nes 81 | ~~~~ 82 | 83 | ## How to benchmark 84 | ### How to use optcarrot as a benchmark 85 | 86 | With `--benchmark` option, Optcarrot works in the headless mode (i.e., no GUI), run a ROM in the first 180 frames, and prints the fps of the last ten frames. 87 | 88 | $ /path/to/ruby bin/optcarrot --benchmark examples/Lan_Master.nes 89 | fps: 26.74081335620352 90 | checksum: 59662 91 | 92 | Or, you may want to use `bin/optcarrot-bench`. 93 | 94 | $ /path/to/ruby bin/optcarrot-bench # measure average FPS for frames 171--180 95 | $ /path/to/ruby bin/optcarrot-bench3000 # measure average FPS for frames 2991--3000 96 | 97 | By default, Optcarrot depends upon [ffi] gem. The headless mode has *zero* dependency: no gems, no external libraries, even no stdlib are required. Unfortunately, you need to use [`tools/shim.rb`](../tools/shim.rb) due to some incompatibilities between MRI and other implementations. 98 | 99 | $ jruby -r ./tools/shim.rb -Ilib bin/optcarrot --benchmark examples/Lan_Master.nes 100 | 101 | ### How to run the full benchmark 102 | 103 | This script will build docker images for some Ruby implementations, run a benchmark on them, and create `benchmark/bm-latest.csv`. 104 | 105 | $ ruby tools/run-benchmark.rb all -m all -c 10 106 | $ ruby tools/run-benchmark.rb mastermjit,master,ruby27mjit,ruby27,ruby20,truffleruby,jruby,topaz -c 10 -m all -f 3000 107 | $ ruby tools/plot.rb benchmark/*-oneshot-180.csv benchmark/*-oneshot-3000.csv 108 | 109 | Note that it will take a few hours. If you want to specify target, do: 110 | 111 | $ ruby tools/run-benchmark.rb ruby24 -m all 112 | 113 | If you want to try [rubyomr-preview][omr], you need to load its docker image before running the benchmark. 114 | 115 | [ffi]: http://rubygems.org/gems/ffi 116 | [omr]: https://github.com/rubyomr-preview/rubyomr-preview 117 | -------------------------------------------------------------------------------- /doc/bonus.md: -------------------------------------------------------------------------------- 1 | # Optcarrot ProTips™ 2 | ## How to install SDL2 3 | 4 | If you are using Debian/Ubuntu, just do: 5 | 6 | $ sudo apt-get install libsdl2-dev 7 | 8 | In Windows, get [`SDL2.dll`](https://www.libsdl.org/), put it into the current directory, and run Optcarrot. 9 | 10 | ## Advanced usage 11 | 12 | ### How to test Optcarrot 13 | 14 | $ ruby tools/run-tests.rb 15 | 16 | ### How to profile Optcarrot 17 | 18 | You can use [stackprof](https://github.com/tmm1/stackprof). 19 | 20 | $ gem install stackprof 21 | $ bin/optcarrot --benchmark --stackprof examples/Lan_Master.nes 22 | $ stackprof stackprof-cpu.dump 23 | 24 | $ bin/optcarrot --benchmark --stackprof-mode=object examples/Lan_Master.nes 25 | $ stackprof stackprof-object.dump 26 | 27 | ### How to benchmark 28 | 29 | See [`doc/benchmark.md`](benchmark.md). 30 | 31 | ### How to build gem 32 | 33 | $ gem build optcarrot.gemspec 34 | $ gem install optcarrot-*.gem 35 | 36 | ## Supported mappers 37 | 38 | * NROM (0) 39 | * MMC1 (1) 40 | * UxROM (2) 41 | * CNROM (3) 42 | * MMC3 (4) 43 | 44 | ## Joke features 45 | ### ZIP reading 46 | 47 | Optcarrot supports loading a ROM in a ZIP file. `zlib` library is required. 48 | 49 | $ bin/optcarrot examples/alter_ego.zip 50 | 51 | (`Optcarrot::ROM.zip_extract` in `lib/optcarrot/rom.rb` parses a ZIP file.) 52 | 53 | ### PNG/GIF/Sixel video output 54 | 55 | $ bin/optcarrot --video=png --video-output=foo.png -f 30 examples/Lan_Master.nes 56 | $ bin/optcarrot --video=gif --video-output=foo.gif -f 30 examples/Lan_Master.nes 57 | $ bin/optcarrot --video=sixel --audio=ao --input=term examples/Lan_Master.nes 58 | 59 | Each encoder is implemented in `lib/optcarrot/driver/*_video.rb`. 60 | 61 | ## ROM Reader 62 | 63 | You *must* get a commercial ROM in a legal way. You can buy a cartridge, and read ROM data from it. (I heard this is legal since NES cartridges are not encrypted at all, but I am not a laywer. Do at your own risk.) 64 | 65 | I created my own ROM reader based on ["HongKong with Arduino"](http://hongkongarduino.web.fc2.com/). See also `tools/reader.rb`. It requires `arduino_firmata`. 66 | 67 | Or, there are [many interesting *free* ROMs](http://www.romhacking.net/homebrew/) that fans created. Some of them are bundled in `examples/` directory. 68 | 69 | ## The meaning of Optcarrot 70 | 71 | OPTimization carrot. Ruby developers will obtain a reward (able to play NES games!) if they successfully achieve Ruby3x3. 72 | -------------------------------------------------------------------------------- /doc/internal.md: -------------------------------------------------------------------------------- 1 | # Optcarrot Internal 2 | 3 | ## NES architecture 4 | 5 | +-CARTRIDGE--------------+ 6 | | | 7 | | [PRG ROM] [CHR ROM] | 8 | | | | | 9 | +-------|----------|-----+ 10 | | | 11 | +-NES---|----------|-----+ 12 | | | | | 13 | audio----[APU/CPU]------[PPU]------video 14 | | | | | 15 | | [RAM] [VRAM] | 16 | | | 17 | +------------------------+ 18 | 19 | * NES 20 | * CPU: Central Processing Unit (1.8 MHz) 21 | * PPU: Picture Processing Unit (5.3 MHz: CPU clock x 3) 22 | * Generates NTSC video output 23 | * APU: Audio Processing Unit (1.8 MHz) 24 | * Generates audio wave 25 | * RAM: Main memory (2 kB) 26 | * VRAM: Video memory (2 kB) 27 | 28 | * Cartridge 29 | * PRG ROM: Program Memory 30 | * CHR ROM: Character Memory (dot-picture) 31 | 32 | ## Modules 33 | 34 | ### Main 35 | 36 | * Optcarrot::NES (in lib/optcarrot/nes.rb) 37 | 38 | This connects CPU, PPU, APU, peripherals, and frontend drivers (such as SDL2). 39 | Stackprof is managed in this module. 40 | 41 | ### Core 42 | 43 | * Optcarrot::CPU (in lib/optcarrot/cpu.rb) 44 | * Optcarrot::PPU (in lib/optcarrot/ppu.rb) 45 | * Optcarrot::APU (in lib/optcarrot/apu.rb) 46 | 47 | These modules emulate CPU, PPU, and APU, respectively. In principle, they does not depend on a specific frontend. 48 | 49 | CPU and PPU have a inner class `OptimizedCodeBuilder` that creates the source code of the generated core (see later). 50 | It parses the source code itself with assumption that the indent is sane. So, be careful to modify the source code of CPU and PPU. 51 | 52 | ### Peripherals 53 | 54 | * Optcarrot::Pad (in lib/optcarrot/pad.rb) 55 | 56 | This emulates a game pad. This module itself does not depend on a specific frontend. 57 | 58 | * Optcarrot::ROM (in lib/optcarrot/rom.rb) 59 | 60 | This emulates a cartridge. Optcarrot::ROM itself emulates NROM mappers. 61 | It is carefully designed so that other NES mappers can be defined by extending this module. 62 | Actually `lib/optcarrot/mapper/*.rb` are defined in this way. 63 | 64 | ### Frontend 65 | 66 | * Optcarrot::Driver (in lib/optcarrot/driver.rb) 67 | 68 | This file includes abstract classes for user frontend. 69 | Actual frontends are defined in `lib/optcarrot/driver/*.rb`. 70 | 71 | A frontend consists of `Video`, `Audio`, and `Input` drivers. 72 | Basically, a user can combine a favorite drivers. But some drivers are tied to another specific drivers, e.g., `SFMLInput` can be used only when `SFMLVideo` is used. 73 | 74 | * Optcarrot::Config (in lib/optcarrot/config.rb) 75 | 76 | This serves as a configuration manager with a command-line option parser. 77 | 78 | ### Helpers 79 | 80 | * Optcarrot::CodeOptimizationHelper (in lib/optcarrot/opt.rb) 81 | 82 | This module provides some helper methods to manipulate source code. 83 | 84 | * Optcarrot::Palette (in lib/optcarrot/palette.rb) 85 | 86 | This generates a palette data. 87 | 88 | ## Two "cores" of Optcarrot 89 | 90 | The performance bottleneck of Optcarrot is PPU emulation. It takes about 80% of the execution time. 91 | 92 | Optcarrot has two PPU emulation cores: the default core and the optimized core. 93 | 94 | * The default core: Slow, but its source code is (relatively) clean by using Fiber. 95 | 96 | * The optimized core: Fast, but it source code is super-dirty. 97 | It consists of a while-loop that includes one big case-when statement. 98 | 99 | Casual Ruby users should write clean code. So Ruby should aim to achieve 60 fps by the default core in future. 100 | The optimized core is a play ground to research a promising approach to improve the performance of Ruby implementations. 101 | 102 | CPU emulation is the second bottleneck. Optcarrot also has two CPU emulation core in the same fashion. 103 | 104 | ## Optimized core 105 | 106 | The source code of optimized core is dynamically generated. Optcarrot performs the following steps at the startup: 107 | 108 | 1. Read the source code of the default core, i.e., `s = File.read(__FILE__)` 109 | 2. Apply a series of string manipulations and generate the source code, i.e., `s = s.gsub(...)` 110 | 3. Load the generated source code, i.e., `eval(s)` 111 | 112 | The actual generators are `PPU::OptimizedCodeBuilder` and `CPU::OptimizedCodeBuilder`. 113 | 114 | In step 2, some optimizations, e.g., method inlining and easy pre-computation, are applied. 115 | You can see the list of available optimizations by a command-line option `--list-opts`. 116 | 117 | $ bin/optcarrot --list-opts 118 | 119 | The meanings of each optimization are shown in the last of this document. 120 | 121 | ## Optimization tuning 122 | 123 | You can enable/disable each optimization by `--opt-ppu` and `--opt-cpu`. 124 | 125 | # Use the generated core with optimizations `method_inlining' and `split_show_mode' enabled 126 | $ bin/optcarrot --opt-ppu=method_inlining,split_show_mode ... [ROM file] 127 | 128 | # Use the generated core with all optimizations 129 | $ bin/optcarrot --opt-ppu=all [ROM file] 130 | 131 | # Use the generated core with all optimizations but `method_inlining' 132 | $ bin/optcarrot --opt-ppu=-method_inlining ... [ROM file] 133 | 134 | # Use the generated core with *no* optimizations 135 | $ bin/optcarrot --opt-ppu=none [ROM file] 136 | 137 | Note that "the generated core with *no* optimizations" is different to "the default core". 138 | The default core uses a Fiber, but the generated core is based on a while-loop. 139 | The performance of them are nearly the same in MRI (but it varys in other Ruby implementations). 140 | 141 | ## Static code generation 142 | 143 | If you want to see the actual source code of the generated core, use `--dump-ppu` or `--dump-cpu`. 144 | 145 | $ bin/optcarrot --dump-ppu 146 | $ bin/optcarrot --opt-ppu=all --dump-ppu 147 | 148 | You can use the dumped core by `--load-ppu` or `--load-cpu`, 149 | 150 | $ bin/optcarrot --dump-ppu > ppu-core.rb 151 | $ bin/optcarrot --load-ppu=ppu-core.rb [ROM file] 152 | 153 | Some incomplete Ruby implementations fail to run the code generator for some reasons. 154 | You can also use this feature in this case. 155 | 156 | ## Basic structure of the generated cores 157 | 158 | PPU: 159 | 160 | def run 161 | while @hclk < @hclk_target 162 | case @hclk 163 | when 0 then ... 164 | when 1 then ... 165 | ... 166 | end 167 | end 168 | end 169 | 170 | CPU: 171 | 172 | def run 173 | while true 174 | @opcode = fetch_pc 175 | case @opcode 176 | when 0x00 then ... 177 | when 0x01 then ... 178 | ... 179 | end 180 | end 181 | end 182 | 183 | ## method inlining 184 | 185 | Before 186 | 187 | case @opcode 188 | when OP_AND 189 | fetch 190 | execute_and 191 | store 192 | ... 193 | end 194 | 195 | After 196 | 197 | case @opcode 198 | when OP_AND 199 | # fetch 200 | @operand = @mem[@addr] 201 | 202 | # execute_and 203 | @operand &= @A 204 | 205 | # store 206 | @mem[@addr] = @operand 207 | ... 208 | end 209 | 210 | ## constant inlining 211 | 212 | Before 213 | 214 | case @opcode 215 | when OP_AND then ... 216 | when OP_OR then ... 217 | when OP_EOR then ... 218 | ... 219 | end 220 | 221 | After 222 | 223 | case @opcode 224 | when 0x29 then ... 225 | when 0x09 then ... 226 | when 0x49 then ... 227 | ... 228 | end 229 | 230 | ## ivar localization 231 | 232 | Before 233 | 234 | def run 235 | while @hclk < @hclk_target 236 | case @hclk 237 | when 0 then ... 238 | when 1 then ... 239 | ... 240 | end 241 | end 242 | end 243 | 244 | After 245 | 246 | def run 247 | __hclk__ = @hclk 248 | __hclk_target__ = @hclk_target 249 | 250 | while __hclk__ < __hclk_target__ 251 | case __hclk__ 252 | ... 253 | end 254 | end 255 | 256 | ensure 257 | @hclk = __hclk__ 258 | @hclk_target = __hclk_target__ 259 | end 260 | 261 | ## split path 262 | 263 | Before 264 | 265 | def run 266 | while @hclk < @hclk_target 267 | case @hclk 268 | when 0 269 | clk_0 if @enabled 270 | when 1 271 | clk_1 if @enabled 272 | ... 273 | end 274 | end 275 | end 276 | 277 | After 278 | 279 | def run 280 | if @enabled 281 | while @hclk < @hclk_target 282 | case @hclk 283 | when 0 284 | clk_0 285 | when 1 286 | clk_1 287 | ... 288 | end 289 | end 290 | else 291 | while @hclk < @hclk_target 292 | case @hclk 293 | when 0 294 | # skip 295 | when 1 296 | # skip 297 | ... 298 | end 299 | end 300 | end 301 | end 302 | 303 | ## fast path 304 | 305 | Before 306 | 307 | def run 308 | while @hclk < @hclk_target 309 | case @hclk 310 | when 0 311 | clk_0 312 | when 1 313 | clk_1 314 | ... 315 | end 316 | end 317 | end 318 | 319 | After 320 | 321 | def run 322 | while @hclk < @hclk_target 323 | case @hclk 324 | when 0 325 | if @hclk + 8 < @hclk_target 326 | clk_0 327 | clk_1 328 | clk_2 329 | clk_3 330 | clk_4 331 | clk_5 332 | clk_6 333 | clk_7 334 | else 335 | clk_0 336 | end 337 | when 1 338 | clk_1 339 | ... 340 | end 341 | end 342 | end 343 | 344 | ## batch render pixel (w/ fast path) 345 | 346 | Before 347 | 348 | if @hclk + 8 < @hclk_target 349 | clk_0; render_pixel 350 | clk_1; render_pixel 351 | clk_2; render_pixel 352 | clk_3; render_pixel 353 | clk_4; render_pixel 354 | clk_5; render_pixel 355 | clk_6; render_pixel 356 | clk_7; render_pixel 357 | else 358 | clk_0 359 | end 360 | 361 | After 362 | 363 | if @hclk + 8 < @hclk_target 364 | clk_0 365 | clk_1 366 | clk_2 367 | clk_3 368 | clk_4 369 | clk_5 370 | clk_6 371 | clk_7 372 | render_eight_pixels 373 | else 374 | clk_0 375 | end 376 | 377 | ## clock specialization 378 | 379 | Before 380 | 381 | def run 382 | while @hclk < @hclk_target 383 | case @hclk 384 | when 0, 8, 16, 24, 32 385 | foo if @hclk = 16 386 | clk_0_mod_8 387 | ... 388 | end 389 | end 390 | end 391 | 392 | After 393 | 394 | def run 395 | while @hclk < @hclk_target 396 | case @hclk 397 | when 0, 8, 24, 32 398 | clk_0_mod_8 399 | when 16 400 | foo 401 | clk_0_mod_8 402 | ... 403 | end 404 | end 405 | end 406 | 407 | ## oneline 408 | 409 | Before 410 | 411 | def run 412 | while @hclk < @hclk_target 413 | .... 414 | end 415 | end 416 | 417 | After 418 | 419 | def run;while @hclk < @hclk_target;....;end;end 420 | -------------------------------------------------------------------------------- /examples/DABG.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mame/optcarrot/48639d175fce42b6330e47f7ebbe691938752cef/examples/DABG.zip -------------------------------------------------------------------------------- /examples/Lan Master.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mame/optcarrot/48639d175fce42b6330e47f7ebbe691938752cef/examples/Lan Master.zip -------------------------------------------------------------------------------- /examples/Lan_Master.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mame/optcarrot/48639d175fce42b6330e47f7ebbe691938752cef/examples/Lan_Master.nes -------------------------------------------------------------------------------- /examples/alter_ego.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mame/optcarrot/48639d175fce42b6330e47f7ebbe691938752cef/examples/alter_ego.zip -------------------------------------------------------------------------------- /examples/lawn_mower.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mame/optcarrot/48639d175fce42b6330e47f7ebbe691938752cef/examples/lawn_mower.zip -------------------------------------------------------------------------------- /examples/source.yml: -------------------------------------------------------------------------------- 1 | - ROM: alter_ego.zip 2 | License: freeware 3 | URL: http://www.romhacking.net/homebrew/1/ 4 | 5 | - ROM: Lan_Master.nes 6 | Archive: "Lan Master.zip" 7 | License: Public domain 8 | URL: http://www.romhacking.net/homebrew/2/ 9 | 10 | - ROM: lawn_mower.zip 11 | License: Public domain 12 | URL: http://www.romhacking.net/homebrew/42/ 13 | 14 | - ROM: zooming_secretary1-02.zip 15 | License: Creative Commons Attribution license 16 | URL: http://www.romhacking.net/homebrew/3/ 17 | 18 | - ROM: DABG.zip 19 | License: GPL3 20 | URL: http://www.romhacking.net/homebrew/59/ 21 | 22 | - ROM: thwaite-0-03.zip 23 | License: GPL3+ 24 | URL: http://www.romhacking.net/homebrew/10/ 25 | -------------------------------------------------------------------------------- /examples/thwaite-0-03.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mame/optcarrot/48639d175fce42b6330e47f7ebbe691938752cef/examples/thwaite-0-03.zip -------------------------------------------------------------------------------- /examples/zooming_secretary1-02.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mame/optcarrot/48639d175fce42b6330e47f7ebbe691938752cef/examples/zooming_secretary1-02.zip -------------------------------------------------------------------------------- /lib/optcarrot.rb: -------------------------------------------------------------------------------- 1 | # Optcarrot namespace 2 | module Optcarrot 3 | VERSION = "0.9.0" 4 | end 5 | 6 | require_relative "optcarrot/nes" 7 | require_relative "optcarrot/rom" 8 | require_relative "optcarrot/pad" 9 | require_relative "optcarrot/cpu" 10 | require_relative "optcarrot/apu" 11 | require_relative "optcarrot/ppu" 12 | require_relative "optcarrot/palette" 13 | require_relative "optcarrot/driver" 14 | require_relative "optcarrot/config" 15 | -------------------------------------------------------------------------------- /lib/optcarrot/apu.rb: -------------------------------------------------------------------------------- 1 | module Optcarrot 2 | # APU implementation (audio output) 3 | class APU 4 | CLK_M2_MUL = 6 5 | CLK_NTSC = 39_375_000 * CLK_M2_MUL 6 | CLK_NTSC_DIV = 11 7 | 8 | CHANNEL_OUTPUT_MUL = 256 9 | CHANNEL_OUTPUT_DECAY = CHANNEL_OUTPUT_MUL / 4 - 1 10 | 11 | FRAME_CLOCKS = [29830, 1, 1, 29828].map {|n| RP2A03_CC * n } 12 | OSCILLATOR_CLOCKS = [ 13 | [7458, 7456, 7458, 7458], 14 | [7458, 7456, 7458, 7458 + 7452] 15 | ].map {|a| a.map {|n| RP2A03_CC * n } } 16 | 17 | def inspect 18 | "#<#{ self.class }>" 19 | end 20 | 21 | ########################################################################### 22 | # initialization 23 | 24 | def initialize(conf, cpu, rate, bits) 25 | @conf = conf 26 | @cpu = cpu 27 | 28 | @pulse_0, @pulse_1 = Pulse.new(self), Pulse.new(self) 29 | @triangle = Triangle.new(self) 30 | @noise = Noise.new(self) 31 | @dmc = DMC.new(@cpu, self) 32 | @mixer = Mixer.new(@pulse_0, @pulse_1, @triangle, @noise, @dmc) 33 | 34 | @conf.fatal("audio sample rate must be >= 11050") if rate < 11050 35 | @conf.fatal("audio bit depth must be 8 or 16") if bits != 8 && bits != 16 36 | 37 | @settings_rate = rate 38 | 39 | @output = [] 40 | @buffer = [] 41 | 42 | @fixed_clock = 1 43 | @rate_clock = 1 44 | @rate_counter = 0 45 | @frame_counter = 0 46 | @frame_divider = 0 47 | @frame_irq_clock = 0 48 | @frame_irq_repeat = 0 49 | @dmc_clock = 0 50 | 51 | reset(false) 52 | end 53 | 54 | def reset_mapping 55 | @frame_counter /= @fixed_clock 56 | @rate_counter /= @fixed_clock 57 | multiplier = 0 58 | while true 59 | multiplier += 1 60 | break if multiplier >= 512 61 | break if CLK_NTSC * multiplier % @settings_rate == 0 62 | end 63 | @rate_clock = CLK_NTSC * multiplier / @settings_rate 64 | @fixed_clock = CLK_NTSC_DIV * multiplier 65 | @frame_counter *= @fixed_clock 66 | @rate_counter *= @fixed_clock 67 | 68 | @mixer.reset 69 | @buffer.clear 70 | 71 | multiplier = 0 72 | while true 73 | multiplier += 1 74 | break if multiplier >= 0x1000 75 | break if CLK_NTSC * (multiplier + 1) / @settings_rate > 0x7ffff 76 | break if CLK_NTSC * multiplier % @settings_rate == 0 77 | end 78 | rate = CLK_NTSC * multiplier / @settings_rate 79 | fixed = CLK_NTSC_DIV * CPU::CLK_1 * multiplier 80 | 81 | @pulse_0 .update_settings(rate, fixed) 82 | @pulse_1 .update_settings(rate, fixed) 83 | @triangle.update_settings(rate, fixed) 84 | @noise .update_settings(rate, fixed) 85 | 86 | @cpu.add_mappings(0x4000, method(:peek_40xx), @pulse_0 .method(:poke_0)) 87 | @cpu.add_mappings(0x4001, method(:peek_40xx), @pulse_0 .method(:poke_1)) 88 | @cpu.add_mappings(0x4002, method(:peek_40xx), @pulse_0 .method(:poke_2)) 89 | @cpu.add_mappings(0x4003, method(:peek_40xx), @pulse_0 .method(:poke_3)) 90 | @cpu.add_mappings(0x4004, method(:peek_40xx), @pulse_1 .method(:poke_0)) 91 | @cpu.add_mappings(0x4005, method(:peek_40xx), @pulse_1 .method(:poke_1)) 92 | @cpu.add_mappings(0x4006, method(:peek_40xx), @pulse_1 .method(:poke_2)) 93 | @cpu.add_mappings(0x4007, method(:peek_40xx), @pulse_1 .method(:poke_3)) 94 | @cpu.add_mappings(0x4008, method(:peek_40xx), @triangle.method(:poke_0)) 95 | @cpu.add_mappings(0x400a, method(:peek_40xx), @triangle.method(:poke_2)) 96 | @cpu.add_mappings(0x400b, method(:peek_40xx), @triangle.method(:poke_3)) 97 | @cpu.add_mappings(0x400c, method(:peek_40xx), @noise .method(:poke_0)) 98 | @cpu.add_mappings(0x400e, method(:peek_40xx), @noise .method(:poke_2)) 99 | @cpu.add_mappings(0x400f, method(:peek_40xx), @noise .method(:poke_3)) 100 | @cpu.add_mappings(0x4010, method(:peek_40xx), @dmc .method(:poke_0)) 101 | @cpu.add_mappings(0x4011, method(:peek_40xx), @dmc .method(:poke_1)) 102 | @cpu.add_mappings(0x4012, method(:peek_40xx), @dmc .method(:poke_2)) 103 | @cpu.add_mappings(0x4013, method(:peek_40xx), @dmc .method(:poke_3)) 104 | @cpu.add_mappings(0x4015, method(:peek_4015), method(:poke_4015)) 105 | @frame_irq_clock = (@frame_counter / @fixed_clock) - CPU::CLK_1 106 | end 107 | 108 | def reset(mapping = true) 109 | @cycles_ratecounter = 0 110 | @frame_divider = 0 111 | @frame_irq_clock = FOREVER_CLOCK 112 | @frame_irq_repeat = 0 113 | @dmc_clock = DMC::LUT[0] 114 | @frame_counter = FRAME_CLOCKS[0] * @fixed_clock 115 | 116 | reset_mapping if mapping 117 | 118 | @pulse_0.reset 119 | @pulse_1.reset 120 | @triangle.reset 121 | @noise.reset 122 | @dmc.reset 123 | @mixer.reset 124 | @buffer.clear 125 | @oscillator_clocks = OSCILLATOR_CLOCKS[0] 126 | end 127 | 128 | ########################################################################### 129 | # other APIs 130 | 131 | attr_reader :output 132 | 133 | def do_clock 134 | clock_dma(@cpu.current_clock) 135 | clock_frame_irq(@cpu.current_clock) if @frame_irq_clock <= @cpu.current_clock 136 | @dmc_clock < @frame_irq_clock ? @dmc_clock : @frame_irq_clock 137 | end 138 | 139 | def clock_dma(clk) 140 | clock_dmc(clk) if @dmc_clock <= clk 141 | end 142 | 143 | def update(target = @cpu.update) 144 | target *= @fixed_clock 145 | proceed(target) 146 | clock_frame_counter if @frame_counter < target 147 | end 148 | 149 | def update_latency 150 | update(@cpu.update + 1) 151 | end 152 | 153 | def update_delta 154 | elapsed = @cpu.update 155 | delta = @frame_counter != elapsed * @fixed_clock 156 | update(elapsed + 1) 157 | delta 158 | end 159 | 160 | def vsync 161 | flush_sound 162 | update(@cpu.current_clock) 163 | frame = @cpu.next_frame_clock 164 | @dmc_clock -= frame 165 | @frame_irq_clock -= frame if @frame_irq_clock != FOREVER_CLOCK 166 | frame *= @fixed_clock 167 | @rate_counter -= frame 168 | @frame_counter -= frame 169 | end 170 | 171 | ########################################################################### 172 | # helpers 173 | 174 | def clock_oscillators(two_clocks) 175 | @pulse_0.clock_envelope 176 | @pulse_1.clock_envelope 177 | @triangle.clock_linear_counter 178 | @noise.clock_envelope 179 | return unless two_clocks 180 | @pulse_0.clock_sweep(-1) 181 | @pulse_1.clock_sweep(0) 182 | @triangle.clock_length_counter 183 | @noise.clock_length_counter 184 | end 185 | 186 | def clock_dmc(target) 187 | begin 188 | if @dmc.clock_dac 189 | update(@dmc_clock) 190 | @dmc.update 191 | end 192 | @dmc_clock += @dmc.freq 193 | @dmc.clock_dma 194 | end while @dmc_clock <= target 195 | end 196 | 197 | def clock_frame_counter 198 | clock_oscillators(@frame_divider[0] == 1) 199 | @frame_divider = (@frame_divider + 1) & 3 200 | @frame_counter += @oscillator_clocks[@frame_divider] * @fixed_clock 201 | end 202 | 203 | def clock_frame_irq(target) 204 | @cpu.do_irq(CPU::IRQ_FRAME, @frame_irq_clock) 205 | begin 206 | @frame_irq_clock += FRAME_CLOCKS[1 + @frame_irq_repeat % 3] 207 | @frame_irq_repeat += 1 208 | end while @frame_irq_clock <= target 209 | end 210 | 211 | def flush_sound 212 | if @buffer.size < @settings_rate / 60 213 | target = @cpu.current_clock * @fixed_clock 214 | proceed(target) 215 | if @buffer.size < @settings_rate / 60 216 | clock_frame_counter if @frame_counter < target 217 | @buffer << @mixer.sample while @buffer.size < @settings_rate / 60 218 | end 219 | end 220 | @output.clear 221 | @output.concat(@buffer) # Array#replace creates an object internally 222 | @buffer.clear 223 | end 224 | 225 | def proceed(target) 226 | while @rate_counter < target && @buffer.size < @settings_rate / 60 227 | @buffer << @mixer.sample 228 | clock_frame_counter if @frame_counter <= @rate_counter 229 | @rate_counter += @rate_clock 230 | end 231 | end 232 | 233 | ########################################################################### 234 | # mapped memory handlers 235 | 236 | # Control 237 | def poke_4015(_addr, data) 238 | update 239 | @pulse_0 .enable(data[0] == 1) 240 | @pulse_1 .enable(data[1] == 1) 241 | @triangle.enable(data[2] == 1) 242 | @noise .enable(data[3] == 1) 243 | @dmc .enable(data[4] == 1) 244 | end 245 | 246 | # Status 247 | def peek_4015(_addr) 248 | elapsed = @cpu.update 249 | clock_frame_irq(elapsed) if @frame_irq_clock <= elapsed 250 | update(elapsed) if @frame_counter < elapsed * @fixed_clock 251 | @cpu.clear_irq(CPU::IRQ_FRAME) | 252 | (@pulse_0 .status ? 0x01 : 0) | 253 | (@pulse_1 .status ? 0x02 : 0) | 254 | (@triangle.status ? 0x04 : 0) | 255 | (@noise .status ? 0x08 : 0) | 256 | (@dmc .status ? 0x10 : 0) 257 | end 258 | 259 | # Frame counter (NOTE: this handler is called via Pads) 260 | def poke_4017(_addr, data) 261 | n = @cpu.update 262 | n += CPU::CLK_1 if @cpu.odd_clock? 263 | update(n) 264 | clock_frame_irq(n) if @frame_irq_clock <= n 265 | n += CPU::CLK_1 266 | @oscillator_clocks = OSCILLATOR_CLOCKS[data[7]] 267 | @frame_counter = (n + @oscillator_clocks[0]) * @fixed_clock 268 | @frame_divider = 0 269 | @frame_irq_clock = data & 0xc0 != 0 ? FOREVER_CLOCK : n + FRAME_CLOCKS[0] 270 | @frame_irq_repeat = 0 271 | @cpu.clear_irq(CPU::IRQ_FRAME) if data[6] != 0 272 | clock_oscillators(true) if data[7] != 0 273 | end 274 | 275 | def peek_40xx(_addr) 276 | 0x40 277 | end 278 | 279 | ########################################################################### 280 | # helper classes 281 | 282 | # A counter for note length 283 | class LengthCounter 284 | LUT = [ 285 | 0x0a, 0xfe, 0x14, 0x02, 0x28, 0x04, 0x50, 0x06, 0xa0, 0x08, 0x3c, 0x0a, 0x0e, 0x0c, 0x1a, 0x0e, 286 | 0x0c, 0x10, 0x18, 0x12, 0x30, 0x14, 0x60, 0x16, 0xc0, 0x18, 0x48, 0x1a, 0x10, 0x1c, 0x20, 0x1e, 287 | ] 288 | def reset 289 | @enabled = false 290 | @count = 0 291 | end 292 | 293 | attr_reader :count 294 | 295 | def enable(enabled) 296 | @enabled = enabled 297 | @count = 0 unless @enabled 298 | @enabled 299 | end 300 | 301 | def write(data, frame_counter_delta) 302 | @count = @enabled ? LUT[data] : 0 if frame_counter_delta || @count == 0 303 | end 304 | 305 | def clock 306 | return false if @count == 0 307 | @count -= 1 308 | return @count == 0 309 | end 310 | end 311 | 312 | # Wave envelope 313 | class Envelope 314 | attr_reader :output, :looping 315 | 316 | def reset_clock 317 | @reset = true 318 | end 319 | 320 | def reset 321 | @output = 0 322 | @count = 0 323 | @volume_base = @volume = 0 324 | @constant = true 325 | @looping = false 326 | @reset = false 327 | update_output 328 | end 329 | 330 | def clock 331 | if @reset 332 | @reset = false 333 | @volume = 0x0f 334 | else 335 | if @count != 0 336 | @count -= 1 337 | return 338 | end 339 | @volume = (@volume - 1) & 0x0f if @volume != 0 || @looping 340 | end 341 | @count = @volume_base 342 | update_output 343 | end 344 | 345 | def write(data) 346 | @volume_base = data & 0x0f 347 | @constant = data[4] == 1 348 | @looping = data[5] == 1 349 | update_output 350 | end 351 | 352 | def update_output 353 | @output = (@constant ? @volume_base : @volume) * CHANNEL_OUTPUT_MUL 354 | end 355 | end 356 | 357 | # Mixer (with DC Blocking filter) 358 | class Mixer 359 | VOL = 192 360 | P_F = 900 361 | P_0 = 9552 * CHANNEL_OUTPUT_MUL * VOL * (P_F / 100) 362 | P_1 = 8128 * CHANNEL_OUTPUT_MUL * P_F 363 | P_2 = P_F * 100 364 | TND_F = 500 365 | TND_0 = 16367 * CHANNEL_OUTPUT_MUL * VOL * (TND_F / 100) 366 | TND_1 = 24329 * CHANNEL_OUTPUT_MUL * TND_F 367 | TND_2 = TND_F * 100 368 | 369 | def initialize(pulse_0, pulse_1, triangle, noise, dmc) 370 | @pulse_0, @pulse_1, @triangle, @noise, @dmc = pulse_0, pulse_1, triangle, noise, dmc 371 | end 372 | 373 | def reset 374 | @acc = @prev = @next = 0 375 | end 376 | 377 | def sample 378 | dac0 = @pulse_0.sample + @pulse_1.sample 379 | dac1 = @triangle.sample + @noise.sample + @dmc.sample 380 | sample = (P_0 * dac0 / (P_1 + P_2 * dac0)) + (TND_0 * dac1 / (TND_1 + TND_2 * dac1)) 381 | 382 | @acc -= @prev 383 | @prev = sample << 15 384 | @acc += @prev - @next * 3 # POLE 385 | sample = @next = @acc >> 15 386 | 387 | sample = -0x7fff if sample < -0x7fff 388 | sample = 0x7fff if sample > 0x7fff 389 | sample 390 | end 391 | end 392 | 393 | # base class for oscillator channels (Pulse, Triangle, and Noise) 394 | class Oscillator 395 | def inspect 396 | "#<#{ self.class }>" 397 | end 398 | 399 | def initialize(apu) 400 | @apu = apu 401 | @rate = @fixed = 1 402 | @envelope = @length_counter = @wave_length = nil 403 | end 404 | 405 | def reset 406 | @timer = 2048 * @fixed # 2048: reset cycles 407 | @freq = @fixed 408 | @amp = 0 409 | 410 | @wave_length = 0 if @wave_length 411 | @envelope.reset if @envelope 412 | @length_counter.reset if @length_counter 413 | @active = active? 414 | end 415 | 416 | def active? 417 | return false if @length_counter && @length_counter.count == 0 418 | return false if @envelope && @envelope.output == 0 419 | return true 420 | end 421 | 422 | def poke_0(_addr, data) 423 | if @envelope 424 | @apu.update_latency 425 | @envelope.write(data) 426 | @active = active? 427 | end 428 | end 429 | 430 | def poke_2(_addr, data) 431 | @apu.update 432 | if @wave_length 433 | @wave_length = (@wave_length & 0x0700) | (data & 0x00ff) 434 | update_freq 435 | end 436 | end 437 | 438 | def poke_3(_addr, data) 439 | delta = @apu.update_delta 440 | if @wave_length 441 | @wave_length = (@wave_length & 0x00ff) | ((data & 0x07) << 8) 442 | update_freq 443 | end 444 | @envelope.reset_clock if @envelope 445 | @length_counter.write(data >> 3, delta) if @length_counter 446 | @active = active? 447 | end 448 | 449 | def enable(enabled) 450 | @length_counter.enable(enabled) 451 | @active = active? 452 | end 453 | 454 | def update_settings(r, f) 455 | @freq = @freq / @fixed * f 456 | @timer = @timer / @fixed * f 457 | @rate, @fixed = r, f 458 | end 459 | 460 | def status 461 | @length_counter.count > 0 462 | end 463 | 464 | def clock_envelope 465 | @envelope.clock 466 | @active = active? 467 | end 468 | end 469 | 470 | #-------------------------------------------------------------------------- 471 | 472 | ### Pulse channel ### 473 | class Pulse < Oscillator 474 | MIN_FREQ = 0x0008 475 | MAX_FREQ = 0x07ff 476 | WAVE_FORM = [0b11111101, 0b11111001, 0b11100001, 0b00000110].map {|n| (0..7).map {|i| n[i] * 0x1f } } 477 | 478 | def initialize(_apu) 479 | super 480 | @wave_length = 0 481 | @envelope = Envelope.new 482 | @length_counter = LengthCounter.new 483 | end 484 | 485 | def reset 486 | super 487 | @freq = @fixed * 2 488 | @valid_freq = false 489 | @step = 0 490 | @form = WAVE_FORM[0] 491 | @sweep_rate = 0 492 | @sweep_count = 1 493 | @sweep_reload = false 494 | @sweep_increase = -1 495 | @sweep_shift = 0 496 | end 497 | 498 | def active? 499 | super && @valid_freq 500 | end 501 | 502 | def update_freq 503 | if @wave_length >= MIN_FREQ && @wave_length + (@sweep_increase & @wave_length >> @sweep_shift) <= MAX_FREQ 504 | @freq = (@wave_length + 1) * 2 * @fixed 505 | @valid_freq = true 506 | else 507 | @valid_freq = false 508 | end 509 | @active = active? 510 | end 511 | 512 | def poke_0(_addr, data) 513 | super 514 | @form = WAVE_FORM[data >> 6 & 3] 515 | end 516 | 517 | def poke_1(_addr, data) 518 | @apu.update 519 | @sweep_increase = data[3] != 0 ? 0 : -1 520 | @sweep_shift = data & 0x07 521 | @sweep_rate = 0 522 | if data[7] == 1 && @sweep_shift > 0 523 | @sweep_rate = ((data >> 4) & 0x07) + 1 524 | @sweep_reload = true 525 | end 526 | update_freq 527 | end 528 | 529 | def poke_3(_addr, _data) 530 | super 531 | @step = 0 532 | end 533 | 534 | def clock_sweep(complement) 535 | @active = false if !@envelope.looping && @length_counter.clock 536 | if @sweep_rate != 0 537 | @sweep_count -= 1 538 | if @sweep_count == 0 539 | @sweep_count = @sweep_rate 540 | if @wave_length >= MIN_FREQ 541 | shifted = @wave_length >> @sweep_shift 542 | if @sweep_increase == 0 543 | @wave_length += complement - shifted 544 | update_freq 545 | elsif @wave_length + shifted <= MAX_FREQ 546 | @wave_length += shifted 547 | update_freq 548 | end 549 | end 550 | end 551 | end 552 | 553 | return unless @sweep_reload 554 | 555 | @sweep_reload = false 556 | @sweep_count = @sweep_rate 557 | end 558 | 559 | def sample 560 | sum = @timer 561 | @timer -= @rate 562 | if @active 563 | if @timer < 0 564 | sum >>= @form[@step] 565 | begin 566 | v = -@timer 567 | v = @freq if v > @freq 568 | sum += v >> @form[@step = (@step + 1) & 7] 569 | @timer += @freq 570 | end while @timer < 0 571 | @amp = (sum * @envelope.output + @rate / 2) / @rate 572 | else 573 | @amp = @envelope.output >> @form[@step] 574 | end 575 | else 576 | if @timer < 0 577 | count = (-@timer + @freq - 1) / @freq 578 | @step = (@step + count) & 7 579 | @timer += count * @freq 580 | end 581 | return 0 if @amp < CHANNEL_OUTPUT_DECAY 582 | @amp -= CHANNEL_OUTPUT_DECAY 583 | end 584 | @amp 585 | end 586 | end 587 | 588 | #-------------------------------------------------------------------------- 589 | 590 | ### Triangle channel ### 591 | class Triangle < Oscillator 592 | MIN_FREQ = 2 + 1 593 | WAVE_FORM = (0..15).to_a + (0..15).to_a.reverse 594 | 595 | def initialize(_apu) 596 | super 597 | @wave_length = 0 598 | @length_counter = LengthCounter.new 599 | end 600 | 601 | def reset 602 | super 603 | @step = 7 604 | @status = :counting 605 | @linear_counter_load = 0 606 | @linear_counter_start = true 607 | @linear_counter = 0 608 | end 609 | 610 | def active? 611 | super && @linear_counter != 0 && @wave_length >= MIN_FREQ 612 | end 613 | 614 | def update_freq 615 | @freq = (@wave_length + 1) * @fixed 616 | @active = active? 617 | end 618 | 619 | def poke_0(_addr, data) 620 | super 621 | @apu.update 622 | @linear_counter_load = data & 0x7f 623 | @linear_counter_start = data[7] == 0 624 | end 625 | 626 | def poke_3(_addr, _data) 627 | super 628 | @status = :reload 629 | end 630 | 631 | def clock_linear_counter 632 | if @status == :counting 633 | @linear_counter -= 1 if @linear_counter != 0 634 | else 635 | @status = :counting if @linear_counter_start 636 | @linear_counter = @linear_counter_load 637 | end 638 | @active = active? 639 | end 640 | 641 | def clock_length_counter 642 | @active = false if @linear_counter_start && @length_counter.clock 643 | end 644 | 645 | def sample 646 | if @active 647 | sum = @timer 648 | @timer -= @rate 649 | if @timer < 0 650 | sum *= WAVE_FORM[@step] 651 | begin 652 | v = -@timer 653 | v = @freq if v > @freq 654 | sum += v * WAVE_FORM[@step = (@step + 1) & 0x1f] 655 | @timer += @freq 656 | end while @timer < 0 657 | @amp = (sum * CHANNEL_OUTPUT_MUL + @rate / 2) / @rate * 3 658 | else 659 | @amp = WAVE_FORM[@step] * CHANNEL_OUTPUT_MUL * 3 660 | end 661 | else 662 | return 0 if @amp < CHANNEL_OUTPUT_DECAY 663 | @amp -= CHANNEL_OUTPUT_DECAY 664 | @step = 0 665 | end 666 | @amp 667 | end 668 | end 669 | 670 | #-------------------------------------------------------------------------- 671 | 672 | ### Noise channel ### 673 | class Noise < Oscillator 674 | LUT = [4, 8, 16, 32, 64, 96, 128, 160, 202, 254, 380, 508, 762, 1016, 2034, 4068] 675 | NEXT_BITS_1, NEXT_BITS_6 = [1, 6].map do |shifter| 676 | (0..0x7fff).map {|bits| bits[0] == bits[shifter] ? bits / 2 : bits / 2 + 0x4000 } 677 | end 678 | 679 | def initialize(_apu) 680 | super 681 | @envelope = Envelope.new 682 | @length_counter = LengthCounter.new 683 | end 684 | 685 | def reset 686 | super 687 | @freq = LUT[0] * @fixed 688 | @bits = 0x4000 689 | @shifter = NEXT_BITS_1 690 | end 691 | 692 | def poke_2(_addr, data) 693 | @apu.update 694 | @freq = LUT[data & 0x0f] * @fixed 695 | @shifter = data[7] != 0 ? NEXT_BITS_6 : NEXT_BITS_1 696 | end 697 | 698 | def clock_length_counter 699 | @active = false if !@envelope.looping && @length_counter.clock 700 | end 701 | 702 | def sample 703 | @timer -= @rate 704 | if @active 705 | return @bits.even? ? @envelope.output * 2 : 0 if @timer >= 0 706 | 707 | sum = @bits.even? ? (@timer + @rate) : 0 708 | begin 709 | @bits = @shifter[@bits] 710 | if @bits.even? 711 | v = -@timer 712 | v = @freq if v > @freq 713 | sum += v 714 | end 715 | @timer += @freq 716 | end while @timer < 0 717 | return (sum * @envelope.output + @rate / 2) / @rate * 2 718 | else 719 | while @timer < 0 720 | @bits = @shifter[@bits] 721 | @timer += @freq 722 | end 723 | return 0 724 | end 725 | end 726 | end 727 | 728 | #-------------------------------------------------------------------------- 729 | 730 | ### DMC channel ### 731 | class DMC 732 | LUT = [428, 380, 340, 320, 286, 254, 226, 214, 190, 160, 142, 128, 106, 84, 72, 54].map {|n| n * RP2A03_CC } 733 | 734 | def initialize(cpu, apu) 735 | @apu = apu 736 | @cpu = cpu 737 | @freq = LUT[0] 738 | end 739 | 740 | def reset 741 | @cur_sample = 0 742 | @lin_sample = 0 743 | @freq = LUT[0] 744 | @loop = false 745 | @irq_enable = false 746 | @regs_length_counter = 1 747 | @regs_address = 0xc000 748 | @out_active = false 749 | @out_shifter = 0 750 | @out_dac = 0 751 | @out_buffer = 0x00 752 | @dma_length_counter = 0 753 | @dma_buffered = false 754 | @dma_address = 0xc000 755 | @dma_buffer = 0x00 756 | end 757 | 758 | attr_reader :freq 759 | 760 | def enable(enabled) 761 | @cpu.clear_irq(CPU::IRQ_DMC) 762 | if !enabled 763 | @dma_length_counter = 0 764 | elsif @dma_length_counter == 0 765 | @dma_length_counter = @regs_length_counter 766 | @dma_address = @regs_address 767 | do_dma unless @dma_buffered 768 | end 769 | end 770 | 771 | def sample 772 | if @cur_sample != @lin_sample 773 | step = CHANNEL_OUTPUT_MUL * 8 774 | if @lin_sample + step < @cur_sample 775 | @lin_sample += step 776 | elsif @cur_sample < @lin_sample - step 777 | @lin_sample -= step 778 | else 779 | @lin_sample = @cur_sample 780 | end 781 | end 782 | @lin_sample 783 | end 784 | 785 | def do_dma 786 | @dma_buffer = @cpu.dmc_dma(@dma_address) 787 | @dma_address = 0x8000 | ((@dma_address + 1) & 0x7fff) 788 | @dma_buffered = true 789 | @dma_length_counter -= 1 790 | if @dma_length_counter == 0 791 | if @loop 792 | @dma_address = @regs_address 793 | @dma_length_counter = @regs_length_counter 794 | elsif @irq_enable 795 | @cpu.do_irq(CPU::IRQ_DMC, @cpu.current_clock) 796 | end 797 | end 798 | end 799 | 800 | def update 801 | @cur_sample = @out_dac * CHANNEL_OUTPUT_MUL 802 | end 803 | 804 | def poke_0(_addr, data) 805 | @loop = data[6] != 0 806 | @irq_enable = data[7] != 0 807 | @freq = LUT[data & 0x0f] 808 | @cpu.clear_irq(CPU::IRQ_DMC) unless @irq_enable 809 | end 810 | 811 | def poke_1(_addr, data) 812 | @apu.update 813 | @out_dac = data & 0x7f 814 | update 815 | end 816 | 817 | def poke_2(_addr, data) 818 | @regs_address = 0xc000 | (data << 6) 819 | end 820 | 821 | def poke_3(_addr, data) 822 | @regs_length_counter = (data << 4) + 1 823 | end 824 | 825 | def clock_dac 826 | if @out_active 827 | n = @out_dac + ((@out_buffer & 1) << 2) - 2 828 | @out_buffer >>= 1 829 | if 0 <= n && n <= 0x7f && n != @out_dac 830 | @out_dac = n 831 | return true 832 | end 833 | end 834 | return false 835 | end 836 | 837 | def clock_dma 838 | if @out_shifter == 0 839 | @out_shifter = 7 840 | @out_active = @dma_buffered 841 | if @out_active 842 | @dma_buffered = false 843 | @out_buffer = @dma_buffer 844 | do_dma if @dma_length_counter != 0 845 | end 846 | else 847 | @out_shifter -= 1 848 | end 849 | end 850 | 851 | def status 852 | @dma_length_counter > 0 853 | end 854 | end 855 | end 856 | end 857 | -------------------------------------------------------------------------------- /lib/optcarrot/config.rb: -------------------------------------------------------------------------------- 1 | module Optcarrot 2 | # config manager and logger 3 | class Config 4 | OPTIONS = { 5 | optimization: { 6 | opt_ppu: { 7 | type: :opts, 8 | desc: "select PPU optimizations", 9 | candidates: PPU::OptimizedCodeBuilder::OPTIONS, 10 | default: nil, 11 | }, 12 | opt_cpu: { 13 | type: :opts, 14 | desc: "select CPU optimizations", 15 | candidates: CPU::OptimizedCodeBuilder::OPTIONS, 16 | default: nil, 17 | }, 18 | opt: { shortcut: %w(--opt-ppu=all --opt-cpu=all) }, 19 | list_opts: { type: :info, desc: "list available optimizations" }, 20 | dump_ppu: { type: :info, desc: "print generated PPU source code" }, 21 | dump_cpu: { type: :info, desc: "print generated CPU source code" }, 22 | load_ppu: { type: "FILE", desc: "use generated PPU source code" }, 23 | load_cpu: { type: "FILE", desc: "use generated CPU source code" }, 24 | }, 25 | emulation: { 26 | sprite_limit: { type: :switch, desc: "enable/disable sprite limit", default: false }, 27 | frames: { type: :int, desc: "execute N frames (0 = no limit)", default: 0, aliases: [:f, :frame] }, 28 | audio_sample_rate: { type: :int, desc: "set audio sample rate", default: 44100 }, 29 | audio_bit_depth: { type: :int, desc: "set audio bit depth", default: 16 }, 30 | nestopia_palette: { type: :switch, desc: "use Nestopia palette instead of de facto", default: false }, 31 | }, 32 | driver: { 33 | video: { type: :driver, desc: "select video driver", candidates: Driver::DRIVER_DB[:video].keys }, 34 | audio: { type: :driver, desc: "select audio driver", candidates: Driver::DRIVER_DB[:audio].keys }, 35 | input: { type: :driver, desc: "select input driver", candidates: Driver::DRIVER_DB[:input].keys }, 36 | list_drivers: { type: :info, desc: "print available drivers" }, 37 | sdl2: { shortcut: %w(--video=sdl2 --audio=sdl2 --input=sdl2) }, 38 | sfml: { shortcut: %w(--video=sfml --audio=sfml --input=sfml) }, 39 | headless: { shortcut: %w(--video=none --audio=none --input=none) }, 40 | video_output: { type: "FILE", desc: "save video to file", default: "video.EXT" }, 41 | audio_output: { type: "FILE", desc: "save audio to file", default: "audio.wav" }, 42 | show_fps: { type: :switch, desc: "show fps in the right-bottom corner", default: true }, 43 | key_log: { type: "FILE", desc: "use recorded input file" }, 44 | # key_config: { type: "KEY", desc: "key configuration" }, 45 | }, 46 | profiling: { 47 | print_fps: { type: :switch, desc: "print fps of last 10 frames", default: false }, 48 | print_p95fps: { type: :switch, desc: "print 95th percentile fps", default: false }, 49 | print_fps_history: { type: :switch, desc: "print all fps values for each frame", default: false }, 50 | print_video_checksum: { type: :switch, desc: "print checksum of the last video output", default: false }, 51 | stackprof: { shortcut: "--stackprof-mode=cpu", aliases: :p }, 52 | stackprof_mode: { type: "MODE", desc: "run under stackprof", default: nil }, 53 | stackprof_output: { type: "FILE", desc: "stackprof output file", default: "stackprof-MODE.dump" } 54 | }, 55 | misc: { 56 | benchmark: { shortcut: %w(--headless --print-fps --print-video-checksum --frames 180), aliases: :b }, 57 | loglevel: { type: :int, desc: "set loglevel", default: 1 }, 58 | quiet: { shortcut: "--loglevel=0", aliases: :q }, 59 | verbose: { shortcut: "--loglevel=2", aliases: :v }, 60 | debug: { shortcut: "--loglevel=3", aliases: :d }, 61 | version: { type: :info, desc: "print version" }, 62 | help: { type: :info, desc: "print this message", aliases: :h }, 63 | }, 64 | } 65 | 66 | DEFAULT_OPTIONS = {} 67 | OPTIONS.each_value do |opts| 68 | opts.each do |id, opt| 69 | next if opt[:shortcut] 70 | DEFAULT_OPTIONS[id] = opt[:default] if opt.key?(:default) 71 | attr_reader id 72 | end 73 | end 74 | attr_reader :romfile 75 | 76 | def initialize(opt) 77 | opt = Parser.new(opt).options if opt.is_a?(Array) 78 | DEFAULT_OPTIONS.merge(opt).each {|id, val| instance_variable_set(:"@#{ id }", val) } 79 | end 80 | 81 | def debug(msg) 82 | puts "[DEBUG] " + msg if @loglevel >= 3 83 | end 84 | 85 | def info(msg) 86 | puts "[INFO] " + msg if @loglevel >= 2 87 | end 88 | 89 | def warn(msg) 90 | puts "[WARN] " + msg if @loglevel >= 1 91 | end 92 | 93 | def error(msg) 94 | puts "[ERROR] " + msg 95 | end 96 | 97 | def fatal(msg) 98 | puts "[FATAL] " + msg 99 | abort 100 | end 101 | 102 | # command-line option parser 103 | class Parser 104 | def initialize(argv) 105 | @argv = argv.dup 106 | @options = DEFAULT_OPTIONS.dup 107 | parse_option until @argv.empty? 108 | error "ROM file is not given" unless @options[:romfile] 109 | rescue Invalid => e 110 | puts "[FATAL] #{ e }" 111 | exit 1 112 | end 113 | 114 | attr_reader :options 115 | 116 | class Invalid < RuntimeError; end 117 | 118 | def error(msg) 119 | raise Invalid, msg 120 | end 121 | 122 | def find_option(arg) 123 | OPTIONS.each_value do |opts| 124 | opts.each do |id_base, opt| 125 | [id_base, *opt[:aliases]].each do |id| 126 | id = id.to_s.tr("_", "-") 127 | return opt, id_base if id.size == 1 && arg == "-#{ id }" 128 | return opt, id_base if arg == "--#{ id }" 129 | return opt, id_base, true if opt[:type] == :switch && arg == "--no-#{ id }" 130 | end 131 | end 132 | end 133 | return nil 134 | end 135 | 136 | def parse_option 137 | arg, operand = @argv.shift.split("=", 2) 138 | if arg =~ /\A-(\w{2,})\z/ 139 | args = $1.chars.map {|a| "-#{ a }" } 140 | args.last << "=" << operand if operand 141 | @argv.unshift(*args) 142 | return 143 | end 144 | opt, id, no = find_option(arg) 145 | if opt 146 | if opt[:shortcut] 147 | @argv.unshift(*opt[:shortcut]) 148 | return 149 | elsif opt[:type] == :info 150 | send(id) 151 | exit 152 | elsif opt[:type] == :switch 153 | error "option `#{ arg }' doesn't allow an operand" if operand 154 | @options[id] = !no 155 | else 156 | @options[id] = parse_operand(operand, arg, opt) 157 | end 158 | else 159 | arg = @argv.shift if arg == "--" 160 | error "invalid option: `#{ arg }'" if arg && arg.start_with?("-") 161 | if arg 162 | error "extra argument: `#{ arg }'" if @options[:romfile] 163 | @options[:romfile] = arg 164 | end 165 | end 166 | end 167 | 168 | def parse_operand(operand, arg, opt) 169 | type = opt[:type] 170 | operand ||= @argv.shift 171 | case type 172 | when :opts 173 | operand = operand.split(",").map {|s| s.to_sym } 174 | when :driver 175 | operand = operand.to_sym 176 | error "unknown driver: `#{ operand }'" unless opt[:candidates].include?(operand) 177 | when :int 178 | begin 179 | operand = Integer(operand) 180 | rescue 181 | error "option `#{ arg }' requires numerical operand" 182 | end 183 | end 184 | operand 185 | end 186 | 187 | def help 188 | tbl = ["Usage: #{ $PROGRAM_NAME } [OPTION]... FILE"] 189 | long_name_width = 0 190 | OPTIONS.each do |kind, opts| 191 | tbl << "" << "#{ kind } options:" 192 | opts.each do |id_base, opt| 193 | short_name = [*opt[:aliases]][0] 194 | switch = args = "" 195 | case opt[:type] 196 | when :switch then switch = "[no-]" 197 | when :opts then args = "=OPTS,..." 198 | when :driver then args = "=DRIVER" 199 | when :int then args = "=N" 200 | when String then args = "=" + opt[:type] 201 | end 202 | short_name = "-#{ switch }#{ short_name }, " if short_name && short_name.size == 1 203 | long_name = "--" + switch + id_base.to_s.tr("_", "-") + args 204 | if opt[:shortcut] 205 | desc = "same as `#{ [*opt[:shortcut]].join(" ") }'" 206 | else 207 | desc = opt[:desc] 208 | desc += " (default: #{ opt[:default] || "none" })" if opt.key?(:default) 209 | end 210 | long_name_width = [long_name_width, long_name.size].max 211 | tbl << [short_name, long_name, desc] 212 | end 213 | end 214 | tbl.each do |arg| 215 | if arg.is_a?(String) 216 | puts arg 217 | else 218 | short_name, long_name, desc = arg 219 | puts " %4s%-*s %s" % [short_name, long_name_width, long_name, desc] 220 | end 221 | end 222 | end 223 | 224 | def version 225 | puts "optcarrot #{ VERSION }" 226 | end 227 | 228 | def list_drivers 229 | Driver::DRIVER_DB.each do |kind, drivers| 230 | puts "#{ kind } drivers: #{ drivers.keys * " " }" 231 | end 232 | end 233 | 234 | def list_opts 235 | puts "CPU core optimizations:" 236 | CPU::OptimizedCodeBuilder::OPTIONS.each do |opt| 237 | puts " * #{ opt }" 238 | end 239 | puts 240 | puts "PPU core optimizations:" 241 | PPU::OptimizedCodeBuilder::OPTIONS.each do |opt| 242 | puts " * #{ opt }" 243 | end 244 | puts 245 | puts "(See `doc/internal.md' in detail.)" 246 | end 247 | 248 | def dump_ppu 249 | puts PPU::OptimizedCodeBuilder.new(@options[:loglevel], @options[:opt_ppu] || []).build 250 | end 251 | 252 | def dump_cpu 253 | puts CPU::OptimizedCodeBuilder.new(@options[:loglevel], @options[:opt_cpu] || []).build 254 | end 255 | end 256 | end 257 | end 258 | -------------------------------------------------------------------------------- /lib/optcarrot/driver.rb: -------------------------------------------------------------------------------- 1 | module Optcarrot 2 | # A manager class for drivers (user frontend) 3 | module Driver 4 | DRIVER_DB = { 5 | video: { 6 | sdl2: :SDL2Video, 7 | sfml: :SFMLVideo, 8 | png: :PNGVideo, 9 | gif: :GIFVideo, 10 | sixel: :SixelVideo, 11 | mplayer: :MPlayerVideo, 12 | none: :Video, 13 | }, 14 | audio: { 15 | sdl2: :SDL2Audio, 16 | sfml: :SFMLAudio, 17 | ao: :AoAudio, 18 | wav: :WAVAudio, 19 | none: :Audio, 20 | }, 21 | input: { 22 | sdl2: :SDL2Input, 23 | sfml: :SFMLInput, 24 | term: :TermInput, 25 | log: :LogInput, 26 | none: :Input, 27 | } 28 | } 29 | 30 | module_function 31 | 32 | def load(conf) 33 | video = load_each(conf, :video, conf.video).new(conf) 34 | audio = load_each(conf, :audio, conf.audio).new(conf) 35 | input = load_each(conf, :input, conf.input).new(conf, video) 36 | return video, audio, input 37 | end 38 | 39 | def load_each(conf, type, name) 40 | if name 41 | klass = DRIVER_DB[type][name] 42 | raise "unknown #{ type } driver: #{ name }" unless klass 43 | require_relative "driver/#{ name }_#{ type }" unless name == :none 44 | conf.debug("`#{ name }' #{ type } driver is selected") 45 | Optcarrot.const_get(klass) 46 | else 47 | selected = nil 48 | DRIVER_DB[type].each_key do |n| 49 | begin 50 | selected = load_each(conf, type, n) 51 | break 52 | rescue LoadError 53 | conf.debug("fail to use `#{ n }' #{ type } driver") 54 | end 55 | end 56 | selected 57 | end 58 | end 59 | end 60 | 61 | # A base class of video output driver 62 | class Video 63 | WIDTH = 256 64 | TV_WIDTH = 292 65 | HEIGHT = 224 66 | 67 | def initialize(conf) 68 | @conf = conf 69 | @palette_rgb = @conf.nestopia_palette ? Palette.nestopia_palette : Palette.defacto_palette 70 | @palette = [*0..4096] # dummy palette 71 | init 72 | end 73 | 74 | attr_reader :palette 75 | 76 | def init 77 | @times = [] 78 | end 79 | 80 | def dispose 81 | end 82 | 83 | def tick(_output) 84 | @times << Process.clock_gettime(Process::CLOCK_MONOTONIC) 85 | @times.shift if @times.size > 10 86 | @times.size < 2 ? 0 : ((@times.last - @times.first) / (@times.size - 1)) ** -1 87 | end 88 | 89 | def change_window_size(_scale) 90 | end 91 | 92 | def on_resize(_width, _height) 93 | end 94 | end 95 | 96 | # A base class of audio output driver 97 | class Audio 98 | PACK_FORMAT = { 8 => "c*", 16 => "v*" } 99 | BUFFER_IN_FRAME = 3 # keep audio buffer during this number of frames 100 | 101 | def initialize(conf) 102 | @conf = conf 103 | @rate = conf.audio_sample_rate 104 | @bits = conf.audio_bit_depth 105 | raise "sample bits must be 8 or 16" unless @bits == 8 || @bits == 16 106 | @pack_format = PACK_FORMAT[@bits] 107 | 108 | init 109 | end 110 | 111 | def spec 112 | return @rate, @bits 113 | end 114 | 115 | def init 116 | end 117 | 118 | def dispose 119 | end 120 | 121 | def tick(_output) 122 | end 123 | end 124 | 125 | # A base class of input driver 126 | class Input 127 | def initialize(conf, video) 128 | @conf = conf 129 | @video = video 130 | init 131 | end 132 | 133 | def init 134 | end 135 | 136 | def dispose 137 | end 138 | 139 | def tick(_frame, _pads) 140 | end 141 | 142 | def event(pads, type, code, player) 143 | case code 144 | when :start then pads.send(type, player, Pad::START) 145 | when :select then pads.send(type, player, Pad::SELECT) 146 | when :a then pads.send(type, player, Pad::A) 147 | when :b then pads.send(type, player, Pad::B) 148 | when :right then pads.send(type, player, Pad::RIGHT) 149 | when :left then pads.send(type, player, Pad::LEFT) 150 | when :down then pads.send(type, player, Pad::DOWN) 151 | when :up then pads.send(type, player, Pad::UP) 152 | else 153 | return if type != :keydown 154 | case code 155 | when :screen_x1 then @video.change_window_size(1) 156 | when :screen_x2 then @video.change_window_size(2) 157 | when :screen_x3 then @video.change_window_size(3) 158 | when :screen_full then @video.change_window_size(nil) 159 | when :quit then exit 160 | end 161 | end 162 | end 163 | end 164 | end 165 | -------------------------------------------------------------------------------- /lib/optcarrot/driver/ao_audio.rb: -------------------------------------------------------------------------------- 1 | require "ffi" 2 | 3 | module Optcarrot 4 | # A minimal binding for libao 5 | module Ao 6 | extend FFI::Library 7 | ffi_lib "ao" 8 | 9 | # struct ao_sample_format 10 | class SampleFormat < FFI::Struct 11 | layout( 12 | :bits, :int, 13 | :rate, :int, 14 | :channels, :int, 15 | :byte_format, :int, 16 | :matrix, :pointer, 17 | ) 18 | end 19 | 20 | FMT_NATIVE = 4 21 | 22 | { 23 | initialize: [[], :void], 24 | default_driver_id: [[], :int], 25 | open_live: [[:int, :pointer, :pointer], :pointer], 26 | play: [[:pointer, :pointer, :int], :uint32, { blocking: true }], 27 | close: [[:pointer], :int], 28 | shutdown: [[], :void], 29 | }.each do |name, params| 30 | opt = params.last.is_a?(Hash) ? params.pop : {} 31 | attach_function(name, :"ao_#{ name }", *params, **opt) 32 | end 33 | end 34 | 35 | # Audio output driver for libao 36 | class AoAudio < Audio 37 | def init 38 | format = Ao::SampleFormat.new 39 | format[:bits] = @bits 40 | format[:rate] = @rate 41 | format[:channels] = 1 42 | format[:byte_format] = Ao::FMT_NATIVE 43 | format[:matrix] = nil 44 | 45 | Ao.initialize 46 | driver = Ao.default_driver_id 47 | @dev = Ao.open_live(driver, format, nil) 48 | 49 | @conf.fatal("ao_open_live failed") unless @dev 50 | @buff = "".b 51 | end 52 | 53 | def dispose 54 | Ao.close(@dev) 55 | Ao.shutdown 56 | end 57 | 58 | def tick(output) 59 | buff = output.pack(@pack_format) 60 | Ao.play(@dev, buff, buff.bytesize) 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/optcarrot/driver/gif_video.rb: -------------------------------------------------------------------------------- 1 | require_relative "misc" 2 | 3 | module Optcarrot 4 | # Video output driver saving an animation GIF file 5 | class GIFVideo < Video 6 | def init 7 | super 8 | 9 | @f = File.open(File.basename(@conf.video_output) + ".gif", "wb") 10 | 11 | @palette, colors = Driver.quantize_colors(@palette_rgb) 12 | 13 | # GIF Header 14 | header = ["GIF89a", WIDTH, HEIGHT, 0xf7, 0, 0, *colors.flatten] 15 | @f << header.pack("A*vvC*") 16 | 17 | # Application Extension 18 | @f << [0x21, 0xff, 0x0b, "NETSCAPE", "2.0", 0x03, 0x01, 0x00, 0x00].pack("C3A8A3CCvC") 19 | 20 | # Graphic Control Extension 21 | @header = [0x21, 0xf9, 0x04, 0x00, 1, 255, 0x00].pack("C4vCC") 22 | @header << [0x2c, 0, 0, WIDTH, HEIGHT, 0, 8].pack("Cv4C*") 23 | end 24 | 25 | def dispose 26 | # Trailer 27 | @f << [0x3b].pack("C") 28 | 29 | @f.close 30 | end 31 | 32 | def tick(screen) 33 | compress(screen) 34 | super 35 | end 36 | 37 | def compress(data) 38 | @f << @header 39 | 40 | max_code = 257 41 | dict = (0..max_code).map {|n| [n, []] } 42 | 43 | buff = "" 44 | out = ->(code) { buff << ("%0#{ max_code.bit_length }b" % code).reverse } 45 | 46 | cur_dict = dict 47 | code = nil 48 | out[256] # clear code 49 | data.each do |d| 50 | if cur_dict[d] 51 | code, cur_dict = cur_dict[d] 52 | else 53 | out[code] 54 | if max_code < 4094 55 | max_code += 1 56 | cur_dict[d] = [max_code, []] 57 | end 58 | code, cur_dict = dict[d] 59 | end 60 | end 61 | out[code] 62 | out[257] # end code 63 | 64 | buff = [buff].pack("b*") 65 | 66 | buff = buff.gsub(/.{1,255}/m) { [$&.size].pack("C") + $& } + [0].pack("C") 67 | 68 | @f << buff 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/optcarrot/driver/log_input.rb: -------------------------------------------------------------------------------- 1 | module Optcarrot 2 | # Input driver replaying a recorded input log 3 | class LogInput < Input 4 | def init 5 | @log = @conf.key_log || [] 6 | @log = Marshal.load(File.binread(@log)) if @log.is_a?(String) 7 | @prev_state = 0 8 | end 9 | 10 | attr_writer :log 11 | 12 | def dispose 13 | end 14 | 15 | def tick(frame, pads) 16 | state = @log[frame] || 0 17 | [ 18 | Pad::SELECT, 19 | Pad::START, 20 | Pad::A, 21 | Pad::B, 22 | Pad::RIGHT, 23 | Pad::LEFT, 24 | Pad::DOWN, 25 | Pad::UP, 26 | ].each do |i| 27 | if @prev_state[i] == 0 && state[i] == 1 28 | pads.keydown(0, i) 29 | elsif @prev_state[i] == 1 && state[i] == 0 30 | pads.keyup(0, i) 31 | end 32 | end 33 | @prev_state = state 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/optcarrot/driver/misc.rb: -------------------------------------------------------------------------------- 1 | module Optcarrot 2 | # some helper methods for drivers 3 | module Driver 4 | module_function 5 | 6 | def quantize_colors(colors, limit = 256) 7 | # median-cut 8 | @cubes = [colors.uniq] 9 | (limit - 1).times do 10 | cube = @cubes.pop 11 | axis = (0..2).max_by do |a| 12 | min, max = cube.map {|color| color[a] }.minmax 13 | max - min 14 | end 15 | cube = cube.sort_by {|color| color[axis] } 16 | @cubes << cube[0, cube.size / 2] << cube[(cube.size / 2)..-1] 17 | @cubes.sort_by! {|a| a.size } 18 | end 19 | raise if @cubes.size != limit 20 | idx = 0 21 | mapping = {} 22 | unified_colors = @cubes.map do |cube| 23 | cube.each {|color| mapping[color] = idx } 24 | idx += 1 25 | cube.transpose.map {|ary| ary.inject(&:+) / ary.size } 26 | end 27 | palette = colors.map {|color| mapping[color] } 28 | return palette, unified_colors 29 | end 30 | 31 | def cutoff_overscan(colors) 32 | colors[0, 2048] = EMPTY_ARRAY 33 | colors[-2048, 2048] = EMPTY_ARRAY 34 | end 35 | EMPTY_ARRAY = [] 36 | 37 | SIZE = 1 38 | def show_fps(colors, fps, palette) 39 | digits = fps > 100 ? 3 : 2 40 | w = (3 + digits) * 4 41 | 42 | (223 - 6 * SIZE).upto(223) do |y| 43 | (255 - w * SIZE).upto(255) do |x| 44 | c = colors[idx = x + y * 256] 45 | 46 | # darken the right-bottom corner for drawing FPS 47 | if block_given? 48 | c = yield c 49 | else 50 | r = ((c >> 16) & 0xff) / 4 51 | g = ((c >> 8) & 0xff) / 4 52 | b = ((c >> 0) & 0xff) / 4 53 | c = (c & 0xff000000) | (r << 16) | (g << 8) | b 54 | end 55 | 56 | colors[idx] = c 57 | end 58 | end 59 | 60 | # decide fps color 61 | color = 62 | case 63 | when fps >= 90 then palette[0x19] # green 64 | when fps >= 60 then palette[0x11] # blue 65 | when fps >= 55 then palette[0x28] # yellow 66 | else palette[0x16] # red 67 | end 68 | 69 | # draw FPS 70 | (3 + digits).times do |i| # show "xxFPS" 71 | bits = FONT[i < digits ? fps / 10**(digits - i - 1) % 10 : i - digits + 10] 72 | 5.times do |y| 73 | 3.times do |x| 74 | SIZE.times do |dy| 75 | SIZE.times do |dx| 76 | if bits[x + y * 3] == 1 77 | colors[(224 + (y - 6) * SIZE + dy) * 256 + (256 + i * 4 + x - w) * SIZE + dx] = color 78 | end 79 | end 80 | end 81 | end 82 | end 83 | end 84 | end 85 | 86 | # tiny font data for fps 87 | FONT = [ 88 | 0b111_101_101_101_111, # '0' 89 | 0b111_010_010_011_010, # '1' 90 | 0b111_001_111_100_111, # '2' 91 | 0b111_100_111_100_111, # '3' 92 | 0b100_100_111_101_101, # '4' 93 | 0b111_100_111_001_111, # '5' 94 | 0b111_101_111_001_111, # '6' 95 | 0b010_010_100_101_111, # '7' 96 | 0b111_101_111_101_111, # '8' 97 | 0b111_100_111_101_111, # '9' 98 | 0b001_001_111_001_111, # 'F' 99 | 0b001_011_101_101_011, # 'P' 100 | 0b011_100_010_001_110, # 'S' 101 | ] 102 | 103 | # icon data 104 | def icon_data 105 | width, height = 16, 16 106 | pixels = FFI::MemoryPointer.new(:uint8, width * height * 4) 107 | 108 | palette = [ 109 | 0x00000000, 0xff0026ff, 0xff002cda, 0xff004000, 0xff0050ff, 0xff006000, 0xff007aff, 0xff00a000, 0xff00a4ff, 110 | 0xff00e000, 0xff4f5600, 0xffa0a000, 0xffe0e000 111 | ] 112 | dat = "38*2309(3:9&,8210982(32,=&8*1:=2,9=1#5$(2&3'?%(-@715+)A3'?'A-.<0$$++B1:$?B6<0$++)$43#%)'A@<:%B314@.<1" 113 | i = 66 114 | "54'4-6>')+((;/7#0#,2,*//..'$%-11*(00##".scan(/../) do 115 | dat = dat.gsub(i.chr, $&) 116 | i -= 1 117 | end 118 | dat = dat.bytes.map {|clr| palette[clr - 35] } 119 | 120 | return width, height, pixels.write_bytes(dat.pack("V*")) 121 | end 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /lib/optcarrot/driver/mplayer_video.rb: -------------------------------------------------------------------------------- 1 | require_relative "misc" 2 | 3 | module Optcarrot 4 | # Video output driver using mplayer 5 | # Inspired from https://github.com/polmuz/pypy-image-demo/blob/master/io.py 6 | class MPlayerVideo < Video 7 | MAX_FPS = NES::FPS 8 | 9 | def init 10 | super 11 | @mplayer = IO.popen("mplayer -really-quiet -noframedrop -vf scale - 2>/dev/null", "wb") 12 | @mplayer.puts("YUV4MPEG2 W#{ WIDTH } H#{ HEIGHT } F#{ MAX_FPS }:1 Ip A#{ TV_WIDTH }:#{ WIDTH } C444") 13 | 14 | @palette = @palette_rgb.map do |r, g, b| 15 | # From https://en.wikipedia.org/wiki/YCbCr#JPEG_conversion 16 | y = (+0.299 * r + 0.587 * g + 0.114 * b).to_i + 0 17 | cb = (-0.168736 * r - 0.331264 * g + 0.5 * b).to_i + 128 18 | cr = (+0.5 * r - 0.418688 * g - 0.081312 * b).to_i + 128 19 | [y, cr, cb] 20 | end 21 | end 22 | 23 | def dispose 24 | @mplayer.close 25 | end 26 | 27 | def tick(screen) 28 | @mplayer.write "FRAME\n" 29 | 30 | Driver.cutoff_overscan(screen) 31 | 32 | if @conf.show_fps && @times.size >= 2 33 | fps = (1.0 / (@times[-1] - @times[-2])).round 34 | Driver.show_fps(screen, fps, @palette) do |y, cr, cb| 35 | [y / 4, cr, cb] 36 | end 37 | end 38 | 39 | colors = screen.map {|a| a[0] } + 40 | screen.map {|a| a[1] } + 41 | screen.map {|a| a[2] } 42 | @mplayer.write colors.pack("C*") 43 | 44 | super 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/optcarrot/driver/png_video.rb: -------------------------------------------------------------------------------- 1 | module Optcarrot 2 | # Video output driver saving a PNG file 3 | class PNGVideo < Video 4 | def init 5 | super 6 | @palette = @palette_rgb 7 | end 8 | 9 | def dispose 10 | return unless @screen && @screen.size >= WIDTH * HEIGHT 11 | bin = PNGEncoder.new(@screen, WIDTH, HEIGHT).encode 12 | File.binwrite(File.basename(@conf.video_output, ".EXT") + ".png", bin) 13 | end 14 | 15 | def tick(screen) 16 | @screen = screen 17 | super 18 | end 19 | 20 | # PNG data generator 21 | class PNGEncoder 22 | def initialize(screen, width, height) 23 | @screen = screen 24 | @width = width 25 | @height = height 26 | end 27 | 28 | def encode 29 | data = [] 30 | @height.times do |y| 31 | data << 0 32 | @width.times do |x| 33 | data.concat(@screen[x + y * @width]) 34 | end 35 | end 36 | 37 | [ 38 | "\x89PNG\r\n\x1a\n".b, 39 | chunk("IHDR", [@width, @height, 8, 2, 0, 0, 0].pack("NNCCCCC")), 40 | chunk("IDAT", cheat_zlib_deflate(data)), 41 | chunk("IEND", ""), 42 | ].join 43 | end 44 | 45 | def chunk(type, data) 46 | [data.bytesize, type, data, crc32(type + data)].pack("NA4A*N") 47 | end 48 | 49 | ADLER_MOD = 65221 50 | def cheat_zlib_deflate(data) 51 | a = 1 52 | b = 0 53 | data.each {|d| b += a += d } 54 | code = [0x78, 0x9c].pack("C2") # Zlib header (RFC 1950) 55 | until data.empty? 56 | s = data.shift(0xffff) 57 | # cheat Deflate (RFC 1951) 58 | code << [data.empty? ? 1 : 0, s.size, ~s.size, *s].pack("CvvC*") 59 | end 60 | code << [b % ADLER_MOD, a % ADLER_MOD].pack("nn") # Adler-32 (RFC 1950) 61 | end 62 | 63 | CRC_TABLE = (0..255).map do |crc| 64 | 8.times {|j| crc ^= 0x1db710641 << j if crc[j] == 1 } 65 | crc >> 8 66 | end 67 | def crc32(data) 68 | crc = 0xffffffff 69 | data.each_byte {|v| crc = (crc >> 8) ^ CRC_TABLE[(crc & 0xff) ^ v] } 70 | crc ^ 0xffffffff 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/optcarrot/driver/sdl2.rb: -------------------------------------------------------------------------------- 1 | require "ffi" 2 | 3 | module Optcarrot 4 | # A minimal binding for SDL2 5 | module SDL2 6 | extend FFI::Library 7 | ffi_lib "SDL2" 8 | 9 | # struct SDL_Version 10 | class Version < FFI::Struct 11 | layout( 12 | :major, :uint8, 13 | :minor, :uint8, 14 | :patch, :uint8, 15 | ) 16 | end 17 | 18 | INIT_TIMER = 0x00000001 19 | INIT_AUDIO = 0x00000010 20 | INIT_VIDEO = 0x00000020 21 | INIT_JOYSTICK = 0x00000200 22 | 23 | # Video 24 | 25 | WINDOWPOS_UNDEFINED = 0x1fff0000 26 | WINDOW_FULLSCREEN = 0x00000001 27 | WINDOW_OPENGL = 0x00000002 28 | WINDOW_SHOWN = 0x00000004 29 | WINDOW_HIDDEN = 0x00000008 30 | WINDOW_BORDERLESS = 0x00000010 31 | WINDOW_RESIZABLE = 0x00000020 32 | WINDOW_MINIMIZED = 0x00000040 33 | WINDOW_MAXIMIZED = 0x00000080 34 | WINDOW_INPUT_GRABBED = 0x00000100 35 | WINDOW_INPUT_FOCUS = 0x00000200 36 | WINDOW_MOUSE_FOCUS = 0x00000400 37 | WINDOW_FULLSCREEN_DESKTOP = (WINDOW_FULLSCREEN | 0x00001000) 38 | 39 | pixels = FFI::MemoryPointer.new(:uint32) 40 | pixels.write_int32(0x04030201) 41 | PACKEDORDER = 42 | case pixels.read_bytes(4).unpack("C*") 43 | when [1, 2, 3, 4] then 3 # PACKEDORDER_ARGB 44 | when [4, 3, 2, 1] then 8 # PACKEDORDER_BGRA 45 | else 46 | raise "unknown endian" 47 | end 48 | 49 | PIXELFORMAT_8888 = 50 | (1 << 28) | 51 | (6 << 24) | # PIXELTYPE_PACKED32 52 | (PACKEDORDER << 20) | 53 | (6 << 16) | # PACKEDLAYOUT_8888 54 | (32 << 8) | # bits 55 | (4 << 0) # bytes 56 | 57 | TEXTUREACCESS_STREAMING = 1 58 | 59 | # Input 60 | 61 | # struct SDL_KeyboardEvent 62 | class KeyboardEvent < FFI::Struct 63 | layout( 64 | :type, :uint32, 65 | :timestamp, :uint32, 66 | :windowID, :uint32, 67 | :state, :uint8, 68 | :repeat, :uint8, 69 | :padding2, :uint8, 70 | :padding3, :uint8, 71 | :scancode, :int, 72 | :sym, :int, 73 | ) 74 | end 75 | 76 | # struct SDL_JoyAxisEvent 77 | class JoyAxisEvent < FFI::Struct 78 | layout( 79 | :type, :uint32, 80 | :timestamp, :uint32, 81 | :which, :uint32, 82 | :axis, :uint8, 83 | :padding1, :uint8, 84 | :padding2, :uint8, 85 | :padding3, :uint8, 86 | :value, :int16, 87 | :padding4, :uint16, 88 | ) 89 | end 90 | 91 | # struct SDL_JoyButtonEvent 92 | class JoyButtonEvent < FFI::Struct 93 | layout( 94 | :type, :uint32, 95 | :timestamp, :uint32, 96 | :which, :uint32, 97 | :button, :uint8, 98 | :state, :uint8, 99 | :padding1, :uint8, 100 | :padding2, :uint8, 101 | ) 102 | end 103 | 104 | # struct SDL_JoyDeviceEvent 105 | class JoyDeviceEvent < FFI::Struct 106 | layout( 107 | :type, :uint32, 108 | :timestamp, :uint32, 109 | :which, :int32, 110 | ) 111 | end 112 | 113 | # Audio 114 | 115 | AUDIO_S8 = 0x8008 116 | AUDIO_S16LSB = 0x8010 117 | AUDIO_S16MSB = 0x9010 118 | 119 | pixels = FFI::MemoryPointer.new(:uint16) 120 | pixels.write_int16(0x0201) 121 | AUDIO_S16SYS = 122 | case pixels.read_bytes(2).unpack("C*") 123 | when [1, 2] then AUDIO_S16LSB 124 | when [2, 1] then AUDIO_S16MSB 125 | else 126 | raise "unknown endian" 127 | end 128 | 129 | # struct SDL_AudioSpec 130 | class AudioSpec < FFI::Struct 131 | layout( 132 | :freq, :int, 133 | :format, :uint16, 134 | :channels, :uint8, 135 | :silence, :uint8, 136 | :samples, :uint16, 137 | :padding, :uint16, 138 | :size, :uint32, 139 | :callback, :pointer, 140 | :userdata, :pointer, 141 | ) 142 | end 143 | 144 | # rubocop:disable Naming/MethodName 145 | def self.AudioCallback(blk) 146 | FFI::Function.new(:void, [:pointer, :pointer, :int], blk) 147 | end 148 | # rubocop:enable Naming/MethodName 149 | 150 | # attach_functions 151 | 152 | functions = { 153 | InitSubSystem: [[:uint32], :int], 154 | QuitSubSystem: [[:uint32], :void, { blocking: true }], 155 | Delay: [[:int], :void, { blocking: true }], 156 | GetError: [[], :string], 157 | GetTicks: [[], :uint32], 158 | 159 | CreateWindow: [[:string, :int, :int, :int, :int, :uint32], :pointer], 160 | DestroyWindow: [[:pointer], :void], 161 | CreateRenderer: [[:pointer, :int, :uint32], :pointer], 162 | DestroyRenderer: [[:pointer], :void], 163 | CreateRGBSurfaceFrom: [[:pointer, :int, :int, :int, :int, :uint32, :uint32, :uint32, :uint32], :pointer], 164 | FreeSurface: [[:pointer], :void], 165 | GetWindowFlags: [[:pointer], :uint32], 166 | SetWindowFullscreen: [[:pointer, :uint32], :int], 167 | SetWindowSize: [[:pointer, :int, :int], :void], 168 | SetWindowTitle: [[:pointer, :string], :void], 169 | SetWindowIcon: [[:pointer, :pointer], :void], 170 | SetHint: [[:string, :string], :int], 171 | RenderSetLogicalSize: [[:pointer, :int, :int], :int], 172 | CreateTexture: [[:pointer, :uint32, :int, :int, :int], :pointer], 173 | DestroyTexture: [[:pointer], :void], 174 | PollEvent: [[:pointer], :int], 175 | UpdateTexture: [[:pointer, :pointer, :pointer, :int], :int], 176 | RenderClear: [[:pointer], :int], 177 | RenderCopy: [[:pointer, :pointer, :pointer, :pointer], :int], 178 | RenderPresent: [[:pointer], :int], 179 | 180 | OpenAudioDevice: [[:string, :int, AudioSpec.ptr, AudioSpec.ptr, :int], :uint32, { blocking: true }], 181 | PauseAudioDevice: [[:uint32, :int], :void, { blocking: true }], 182 | CloseAudioDevice: [[:uint32], :void, { blocking: true }], 183 | 184 | NumJoysticks: [[], :int], 185 | JoystickOpen: [[:int], :pointer], 186 | JoystickClose: [[:pointer], :void], 187 | JoystickNameForIndex: [[:int], :string], 188 | JoystickNumAxes: [[:pointer], :int], 189 | JoystickNumButtons: [[:pointer], :int], 190 | JoystickInstanceID: [[:pointer], :uint32], 191 | 192 | QueueAudio: [[:uint32, :pointer, :int], :int], 193 | GetQueuedAudioSize: [[:uint32], :uint32], 194 | ClearQueuedAudio: [[:uint32], :void], 195 | } 196 | 197 | # check SDL version 198 | 199 | attach_function(:GetVersion, :SDL_GetVersion, [:pointer], :void) 200 | version = Version.new 201 | GetVersion(version) 202 | version = [version[:major], version[:minor], version[:patch]] 203 | if (version <=> [2, 0, 4]) < 0 204 | functions.delete(:QueueAudio) 205 | functions.delete(:GetQueuedAudioSize) 206 | functions.delete(:ClearQueuedAudio) 207 | end 208 | 209 | functions.each do |name, params| 210 | opt = params.last.is_a?(Hash) ? params.pop : {} 211 | attach_function(name, :"SDL_#{ name }", *params, **opt) 212 | end 213 | end 214 | end 215 | -------------------------------------------------------------------------------- /lib/optcarrot/driver/sdl2_audio.rb: -------------------------------------------------------------------------------- 1 | require_relative "sdl2" 2 | 3 | module Optcarrot 4 | # Audio output driver for SDL2 5 | class SDL2Audio < Audio 6 | FORMAT = { 8 => SDL2::AUDIO_S8, 16 => SDL2::AUDIO_S16LSB } 7 | 8 | def init 9 | SDL2.InitSubSystem(SDL2::INIT_AUDIO) 10 | @max_buff_size = @rate * @bits / 8 * BUFFER_IN_FRAME / NES::FPS 11 | 12 | # we need to prevent this callback object from GC 13 | @callback = SDL2.AudioCallback(method(:callback)) 14 | 15 | desired = SDL2::AudioSpec.new 16 | desired[:freq] = @rate 17 | desired[:format] = FORMAT[@bits] 18 | desired[:channels] = 1 19 | desired[:samples] = @rate / 60 * 2 20 | desired[:callback] = defined?(SDL2.QueueAudio) ? nil : @callback 21 | desired[:userdata] = nil 22 | obtained = SDL2::AudioSpec.new 23 | @dev = SDL2.OpenAudioDevice(nil, 0, desired, obtained, 0) 24 | if @dev == 0 25 | @conf.error("SDL2_OpenAudioDevice failed: #{ SDL2.GetError }") 26 | abort 27 | end 28 | @buff = "".b 29 | SDL2.PauseAudioDevice(@dev, 0) 30 | end 31 | 32 | def dispose 33 | SDL2.CloseAudioDevice(@dev) 34 | SDL2.QuitSubSystem(SDL2::INIT_AUDIO) 35 | end 36 | 37 | def tick(output) 38 | buff = output.pack(@pack_format) 39 | if defined?(SDL2.QueueAudio) 40 | SDL2.QueueAudio(@dev, buff, buff.bytesize) 41 | SDL2.ClearQueuedAudio(@dev) if SDL2.GetQueuedAudioSize(@dev) > @max_buff_size 42 | else 43 | @buff << buff 44 | end 45 | end 46 | 47 | # for SDL 2.0.3 or below in that SDL_QueueAudio is not available 48 | def callback(_userdata, stream, stream_len) 49 | buff_size = @buff.size 50 | if stream_len > buff_size 51 | # stream.clear # is it okay? 52 | stream.write_string_length(@buff, buff_size) 53 | @buff.clear 54 | else 55 | stream.write_string_length(@buff, stream_len) 56 | stream_len = buff_size - @max_buff_size if buff_size - stream_len > @max_buff_size 57 | @buff[0, stream_len] = "".freeze 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/optcarrot/driver/sdl2_input.rb: -------------------------------------------------------------------------------- 1 | require_relative "sdl2" 2 | 3 | module Optcarrot 4 | # Input driver for SDL2 5 | class SDL2Input < Input 6 | def init 7 | SDL2.InitSubSystem(SDL2::INIT_JOYSTICK) 8 | @event = FFI::MemoryPointer.new(:uint32, 16) 9 | 10 | @keyboard_repeat_offset = SDL2::KeyboardEvent.offset_of(:repeat) 11 | @keyboard_sym_offset = SDL2::KeyboardEvent.offset_of(:sym) 12 | @joy_which_offset = SDL2::JoyAxisEvent.offset_of(:which) 13 | @joyaxis_axis_offset = SDL2::JoyAxisEvent.offset_of(:axis) 14 | @joyaxis_value_offset = SDL2::JoyAxisEvent.offset_of(:value) 15 | @joybutton_button_offset = SDL2::JoyButtonEvent.offset_of(:button) 16 | 17 | @joysticks = {} 18 | SDL2.NumJoysticks.times do |i| 19 | p SDL2.JoystickNameForIndex(i) 20 | js = SDL2.JoystickOpen(i) 21 | @joysticks[SDL2.JoystickInstanceID(js)] = js 22 | # SDL2.JoystickNumAxes(js) 23 | # SDL2.JoystickNumButtons(js) 24 | end 25 | 26 | @key_mapping = DEFAULT_KEY_MAPPING 27 | end 28 | 29 | def dispose 30 | @joysticks.each_value do |js| 31 | SDL2.JoystickClose(js) 32 | end 33 | @joysticks.clear 34 | SDL2.QuitSubSystem(SDL2::INIT_JOYSTICK) 35 | end 36 | 37 | DEFAULT_KEY_MAPPING = { 38 | 0x20 => [:start, 0], # space 39 | 0x0d => [:select, 0], # return 40 | 0x7a => [:a, 0], # `Z' 41 | 0x78 => [:b, 0], # `X' 42 | 0x4000_004f => [:right, 0], 43 | 0x4000_0050 => [:left, 0], 44 | 0x4000_0051 => [:down, 0], 45 | 0x4000_0052 => [:up, 0], 46 | 47 | # 57 => [:start, 1], # space 48 | # 58 => [:select, 1], # return 49 | # 25 => [:a, 1], # `Z' 50 | # 23 => [:b, 1], # `X' 51 | # 72 => [:right, 1], # right 52 | # 71 => [:left, 1], # left 53 | # 74 => [:down, 1], # down 54 | # 73 => [:up, 1], # up 55 | 56 | 0x31 => [:screen_x1, nil], # `1' 57 | 0x32 => [:screen_x2, nil], # `2' 58 | 0x33 => [:screen_x3, nil], # `3' 59 | 0x66 => [:screen_full, nil], # `f' 60 | 0x71 => [:quit, nil], # `q' 61 | } 62 | 63 | def joystick_move(axis, value, pads) 64 | event(pads, value > 0x7000 ? :keydown : :keyup, axis ? :right : :down, 0) 65 | event(pads, value < -0x7000 ? :keydown : :keyup, axis ? :left : :up, 0) 66 | end 67 | 68 | def joystick_buttondown(button, pads) 69 | case button 70 | when 0 then pads.keydown(0, Pad::A) 71 | when 1 then pads.keydown(0, Pad::B) 72 | when 6 then pads.keydown(0, Pad::SELECT) 73 | when 7 then pads.keydown(0, Pad::START) 74 | end 75 | end 76 | 77 | def joystick_buttonup(button, pads) 78 | case button 79 | when 0 then pads.keyup(0, Pad::A) 80 | when 1 then pads.keyup(0, Pad::B) 81 | when 6 then pads.keyup(0, Pad::SELECT) 82 | when 7 then pads.keyup(0, Pad::START) 83 | end 84 | end 85 | 86 | def tick(_frame, pads) 87 | while SDL2.PollEvent(@event) != 0 88 | case @event.read_int 89 | 90 | when 0x300, 0x301 # SDL_KEYDOWN, SDL_KEYUP 91 | next if @event.get_uint8(@keyboard_repeat_offset) != 0 92 | key = @key_mapping[@event.get_int(@keyboard_sym_offset)] 93 | event(pads, @event.read_int == 0x300 ? :keydown : :keyup, *key) if key 94 | 95 | when 0x600 # SDL_JOYAXISMOTION 96 | which = @event.get_uint32(@joy_which_offset) 97 | if which == 0 # XXX 98 | axis = @event.get_uint8(@joyaxis_axis_offset) == 0 99 | value = @event.get_int16(@joyaxis_value_offset) 100 | joystick_move(axis, value, pads) 101 | end 102 | 103 | when 0x603 # SDL_JOYBUTTONDOWN 104 | which = @event.get_uint32(@joy_which_offset) 105 | joystick_buttondown(@event.get_uint8(@joybutton_button_offset), pads) 106 | 107 | when 0x604 # SDL_JOYBUTTONUP 108 | which = @event.get_uint32(@joy_which_offset) 109 | joystick_buttonup(@event.get_uint8(@joybutton_button_offset), pads) 110 | 111 | when 0x605 # SDL_JOYDEVICEADDED 112 | which = @event.get_uint32(@joy_which_offset) 113 | js = SDL2.JoystickOpen(which) 114 | @joysticks[SDL2.JoystickInstanceID(js)] = js 115 | 116 | when 0x606 # SDL_JOYDEVICEREMOVED 117 | which = @event.get_uint32(@joy_which_offset) 118 | @joysticks.delete(which) 119 | 120 | when 0x100 # SDL_QUIT 121 | exit 122 | end 123 | end 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /lib/optcarrot/driver/sdl2_video.rb: -------------------------------------------------------------------------------- 1 | require_relative "sdl2" 2 | require_relative "misc" 3 | 4 | module Optcarrot 5 | # Video output driver for SDL2 6 | class SDL2Video < Video 7 | def init 8 | SDL2.InitSubSystem(SDL2::INIT_VIDEO) 9 | @ticks_log = [0] * 11 10 | @buf = FFI::MemoryPointer.new(:uint32, WIDTH * HEIGHT) 11 | @titles = (0..99).map {|n| "optcarrot (%d fps)" % n } 12 | 13 | @window = 14 | SDL2.CreateWindow( 15 | "optcarrot", 16 | SDL2::WINDOWPOS_UNDEFINED, 17 | SDL2::WINDOWPOS_UNDEFINED, 18 | TV_WIDTH, HEIGHT, 19 | SDL2::WINDOW_RESIZABLE 20 | ) 21 | @renderer = SDL2.CreateRenderer(@window, -1, 0) 22 | SDL2.SetHint("SDL_RENDER_SCALE_QUALITY", "linear") 23 | SDL2.RenderSetLogicalSize(@renderer, TV_WIDTH, HEIGHT) 24 | @texture = SDL2.CreateTexture( 25 | @renderer, 26 | SDL2::PIXELFORMAT_8888, 27 | SDL2::TEXTUREACCESS_STREAMING, 28 | WIDTH, HEIGHT 29 | ) 30 | 31 | width, height, pixels = Driver.icon_data 32 | @icon = SDL2.CreateRGBSurfaceFrom(pixels, width, height, 32, width * 4, 0x0000ff, 0x00ff00, 0xff0000, 0xff000000) 33 | SDL2.SetWindowIcon(@window, @icon) 34 | 35 | @palette = @palette_rgb.map do |r, g, b| 36 | 0xff000000 | (r << 16) | (g << 8) | b 37 | end 38 | end 39 | 40 | def change_window_size(scale) 41 | if scale 42 | SDL2.SetWindowFullscreen(@window, 0) 43 | SDL2.SetWindowSize(@window, TV_WIDTH * scale, HEIGHT * scale) 44 | elsif SDL2.GetWindowFlags(@window) & SDL2::WINDOW_FULLSCREEN_DESKTOP != 0 45 | SDL2.SetWindowFullscreen(@window, 0) 46 | else 47 | SDL2.SetWindowFullscreen(@window, SDL2::WINDOW_FULLSCREEN_DESKTOP) 48 | end 49 | end 50 | 51 | def dispose 52 | SDL2.FreeSurface(@icon) 53 | SDL2.DestroyTexture(@texture) 54 | SDL2.DestroyRenderer(@renderer) 55 | SDL2.DestroyWindow(@window) 56 | SDL2.QuitSubSystem(SDL2::INIT_VIDEO) 57 | end 58 | 59 | def tick(colors) 60 | prev_ticks = @ticks_log[0] 61 | wait = prev_ticks + 1000 - SDL2.GetTicks * NES::FPS 62 | @ticks_log.rotate!(1) 63 | if wait > 0 64 | SDL2.Delay(wait / NES::FPS) 65 | @ticks_log[0] = prev_ticks + 1000 66 | else 67 | @ticks_log[0] = SDL2.GetTicks * NES::FPS 68 | end 69 | elapsed = (@ticks_log[0] - @ticks_log[1]) / (@ticks_log.size - 1) 70 | fps = (NES::FPS * 1000 + elapsed / 2) / elapsed 71 | fps = 99 if fps > 99 72 | 73 | SDL2.SetWindowTitle(@window, @titles[fps]) 74 | 75 | Driver.cutoff_overscan(colors) 76 | Driver.show_fps(colors, fps, @palette) if @conf.show_fps 77 | 78 | @buf.write_array_of_uint32(colors) 79 | 80 | SDL2.UpdateTexture(@texture, nil, @buf, WIDTH * 4) 81 | SDL2.RenderClear(@renderer) 82 | SDL2.RenderCopy(@renderer, @texture, nil, nil) 83 | SDL2.RenderPresent(@renderer) 84 | 85 | fps 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/optcarrot/driver/sfml.rb: -------------------------------------------------------------------------------- 1 | require "ffi" 2 | 3 | module Optcarrot 4 | # A minimal binding for SFML (CSFML) 5 | module SFML 6 | extend FFI::Library 7 | ffi_lib \ 8 | ["csfml-system", "csfml-system-2"], 9 | ["csfml-window", "csfml-window-2"], 10 | ["csfml-graphics", "csfml-graphics-2"], 11 | ["csfml-audio", "csfml-audio-2"] 12 | 13 | # struct sfVector2u 14 | class Vector2u < FFI::Struct 15 | layout( 16 | :x, :uint, 17 | :y, :uint, 18 | ) 19 | end 20 | 21 | # struct sfVector2f 22 | class Vector2f < FFI::Struct 23 | layout( 24 | :x, :float, 25 | :y, :float, 26 | ) 27 | end 28 | 29 | # struct sfVideoMode 30 | class VideoMode < FFI::Struct 31 | layout( 32 | :width, :int, 33 | :height, :int, 34 | :bits_per_pixel, :int, 35 | ) 36 | end 37 | 38 | # struct sfEvent 39 | class Event < FFI::Struct 40 | layout( 41 | :type, :int, 42 | ) 43 | end 44 | 45 | # struct sfSizeEvent 46 | class SizeEvent < FFI::Struct 47 | layout( 48 | :type, :int, 49 | :width, :uint, 50 | :height, :uint, 51 | ) 52 | end 53 | 54 | # struct sfKeyEvent 55 | class KeyEvent < FFI::Struct 56 | layout( 57 | :type, :int, 58 | :code, :int, 59 | :alt, :int, 60 | :control, :int, 61 | :shift, :int, 62 | :sym, :int, 63 | ) 64 | end 65 | 66 | # struct sfColor 67 | class Color < FFI::Struct 68 | layout( 69 | :r, :uint8, 70 | :g, :uint8, 71 | :b, :uint8, 72 | :a, :uint8, 73 | ) 74 | end 75 | 76 | # struct sfFloatRect 77 | class FloatRect < FFI::Struct 78 | layout( 79 | :left, :float, 80 | :top, :float, 81 | :width, :float, 82 | :height, :float, 83 | ) 84 | end 85 | 86 | # struct sfSoundStreamChunk 87 | class SoundStreamChunk < FFI::Struct 88 | layout( 89 | :samples, :pointer, 90 | :sample_count, :uint, 91 | ) 92 | end 93 | 94 | # rubocop:disable Naming/MethodName 95 | # typedef sfSoundStreamGetDataCallback 96 | def self.SoundStreamGetDataCallback(blk) 97 | FFI::Function.new(:int, [SoundStreamChunk.by_ref, :pointer], blk, blocking: true) 98 | end 99 | # rubocop:enable Naming/MethodName 100 | 101 | attach_function(:sfClock_create, [], :pointer) 102 | attach_function(:sfClock_destroy, [:pointer], :void) 103 | attach_function(:sfClock_getElapsedTime, [:pointer], :int64) 104 | attach_function(:sfClock_restart, [:pointer], :int64) 105 | attach_function(:sfRenderWindow_create, [VideoMode.by_value, :pointer, :uint32, :pointer], :pointer) 106 | attach_function(:sfRenderWindow_clear, [:pointer, Color.by_value], :void) 107 | attach_function(:sfRenderWindow_drawSprite, [:pointer, :pointer, :pointer], :void, blocking: true) 108 | attach_function(:sfRenderWindow_display, [:pointer], :void, blocking: true) 109 | attach_function(:sfRenderWindow_close, [:pointer], :void) 110 | attach_function(:sfRenderWindow_isOpen, [:pointer], :int) 111 | attach_function(:sfRenderWindow_pollEvent, [:pointer, :pointer], :int) 112 | attach_function(:sfRenderWindow_destroy, [:pointer], :void) 113 | attach_function(:sfRenderWindow_setTitle, [:pointer, :pointer], :void) 114 | attach_function(:sfRenderWindow_setSize, [:pointer, Vector2u.by_value], :void) 115 | attach_function(:sfRenderWindow_setFramerateLimit, [:pointer, :int], :void) 116 | attach_function(:sfRenderWindow_setKeyRepeatEnabled, [:pointer, :int], :void) 117 | attach_function(:sfRenderWindow_setView, [:pointer, :pointer], :void) 118 | attach_function(:sfRenderWindow_setIcon, [:pointer, :int, :int, :pointer], :void) 119 | attach_function(:sfTexture_create, [:int, :int], :pointer) 120 | attach_function(:sfTexture_updateFromPixels, [:pointer, :pointer, :int, :int, :int, :int], :void, blocking: true) 121 | attach_function(:sfSprite_create, [], :pointer) 122 | attach_function(:sfSprite_setTexture, [:pointer, :pointer, :int], :void) 123 | attach_function(:sfView_create, [], :pointer) 124 | attach_function(:sfView_createFromRect, [:pointer], :pointer) 125 | attach_function(:sfView_destroy, [:pointer], :void) 126 | attach_function(:sfView_reset, [:pointer, FloatRect.by_value], :void) 127 | attach_function(:sfView_setCenter, [:pointer, Vector2f.by_value], :void) 128 | attach_function(:sfView_setSize, [:pointer, Vector2f.by_value], :void) 129 | attach_function(:sfSoundStream_create, [:pointer, :pointer, :uint, :uint, :pointer], :pointer) 130 | attach_function(:sfSoundStream_destroy, [:pointer], :void, blocking: true) 131 | attach_function(:sfSoundStream_play, [:pointer], :void) 132 | attach_function(:sfSoundStream_stop, [:pointer], :void, blocking: true) 133 | end 134 | end 135 | -------------------------------------------------------------------------------- /lib/optcarrot/driver/sfml_audio.rb: -------------------------------------------------------------------------------- 1 | require_relative "sfml" 2 | 3 | module Optcarrot 4 | # Audio output driver for SFML 5 | class SFMLAudio < Audio 6 | def init 7 | @max_buff_size = @rate * @bits / 8 * BUFFER_IN_FRAME / NES::FPS 8 | 9 | # we need to prevent this callback object from GC 10 | @callback = SFML.SoundStreamGetDataCallback(method(:callback)) 11 | 12 | @stream = SFML.sfSoundStream_create(@callback, nil, 1, @rate, nil) 13 | SFML.sfSoundStream_play(@stream) 14 | @buff = "".b 15 | @cur_buff = FFI::MemoryPointer.new(:char, @max_buff_size + 1) 16 | end 17 | 18 | def dispose 19 | SFML.sfSoundStream_stop(@stream) 20 | SFML.sfSoundStream_destroy(@stream) 21 | end 22 | 23 | def tick(output) 24 | @buff << output.pack("v*".freeze) 25 | end 26 | 27 | # XXX: support 8bit (SFML supports only 16bit, so translation is required) 28 | def callback(chunk, _userdata) 29 | buff_size = @buff.size 30 | if buff_size < @max_buff_size 31 | @cur_buff.put_string(0, @buff) 32 | else 33 | @buff[0, buff_size - @max_buff_size] = "".freeze 34 | @cur_buff.put_string(0, @buff) 35 | buff_size = @max_buff_size 36 | end 37 | if buff_size == 0 38 | @cur_buff.clear 39 | buff_size = @max_buff_size / BUFFER_IN_FRAME 40 | end 41 | chunk[:samples] = @cur_buff 42 | chunk[:sample_count] = buff_size / 2 43 | return 1 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/optcarrot/driver/sfml_input.rb: -------------------------------------------------------------------------------- 1 | require_relative "sfml" 2 | 3 | module Optcarrot 4 | # Input driver for SFML 5 | class SFMLInput < Input 6 | def init 7 | raise "SFMLInput must be used with SFMLVideo" unless @video.is_a?(SFMLVideo) 8 | 9 | @event = FFI::MemoryPointer.new(:uint32, 16) 10 | @keyevent_code_offset = SFML::KeyEvent.offset_of(:code) 11 | @sizeevent_width_offset = SFML::SizeEvent.offset_of(:width) 12 | @sizeevent_height_offset = SFML::SizeEvent.offset_of(:height) 13 | @key_mapping = DEFAULT_KEY_MAPPING 14 | end 15 | 16 | def dispose 17 | end 18 | 19 | DEFAULT_KEY_MAPPING = { 20 | 57 => [:start, 0], # space 21 | 58 => [:select, 0], # return 22 | 25 => [:a, 0], # `Z' 23 | 23 => [:b, 0], # `X' 24 | 72 => [:right, 0], # right 25 | 71 => [:left, 0], # left 26 | 74 => [:down, 0], # down 27 | 73 => [:up, 0], # up 28 | 29 | # 57 => [:start, 1], # space 30 | # 58 => [:select, 1], # return 31 | # 25 => [:a, 1], # `Z' 32 | # 23 => [:b, 1], # `X' 33 | # 72 => [:right, 1], # right 34 | # 71 => [:left, 1], # left 35 | # 74 => [:down, 1], # down 36 | # 73 => [:up, 1], # up 37 | 38 | 27 => [:screen_x1, nil], # `1' 39 | 28 => [:screen_x2, nil], # `2' 40 | 29 => [:screen_x3, nil], # `3' 41 | 5 => [:screen_full, nil], # `f' 42 | 16 => [:quit, nil], # `q' 43 | } 44 | 45 | def tick(_frame, pads) 46 | SFML.sfRenderWindow_setKeyRepeatEnabled(@video.window, 0) 47 | 48 | while SFML.sfRenderWindow_pollEvent(@video.window, @event) != 0 49 | case @event.read_int 50 | when 0 # EvtClosed 51 | SFML.sfRenderWindow_close(@video.window) 52 | exit # tmp 53 | when 1 # EvtResized 54 | w = @event.get_int(@sizeevent_width_offset) 55 | h = @event.get_int(@sizeevent_height_offset) 56 | @video.on_resize(w, h) 57 | when 5 # EvtKeyPressed 58 | event(pads, :keydown, *@key_mapping[@event.get_int(@keyevent_code_offset)]) 59 | when 6 # EvtKeyReleased 60 | event(pads, :keyup, *@key_mapping[@event.get_int(@keyevent_code_offset)]) 61 | when 14 # sfEvtJoystickButtonPressed 62 | # XXX 63 | when 15 # sfEvtJoystickButtonReleased 64 | # XXX 65 | when 16 # sfEvtJoystickMoved 66 | # XXX 67 | when 17 # sfEvtJoystickConnected 68 | # XXX 69 | when 18 # sfEvtJoystickDisconnected 70 | # XXX 71 | end 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/optcarrot/driver/sfml_video.rb: -------------------------------------------------------------------------------- 1 | require_relative "sfml" 2 | require_relative "misc" 3 | 4 | module Optcarrot 5 | # Video output driver for SFML 6 | class SFMLVideo < Video 7 | def init 8 | vm = SFML::VideoMode.new 9 | vm[:width] = TV_WIDTH 10 | vm[:height] = HEIGHT 11 | vm[:bits_per_pixel] = 32 12 | @window = SFML.sfRenderWindow_create(vm, "optcarrot", 7, nil) 13 | @texture = SFML.sfTexture_create(WIDTH, HEIGHT) 14 | @sprite = SFML.sfSprite_create 15 | SFML.sfRenderWindow_setFramerateLimit(@window, 60) 16 | SFML.sfSprite_setTexture(@sprite, @texture, 1) 17 | @color = SFML::Color.new 18 | @color[:r] = @color[:g] = @color[:b] = 0 19 | @color[:a] = 255 20 | @buf = FFI::MemoryPointer.new(:uint8, WIDTH * HEIGHT * 4) 21 | 22 | width, height, pixels = Driver.icon_data 23 | SFML.sfRenderWindow_setIcon(@window, width, height, pixels) 24 | 25 | @frame = 0 26 | @fps = 0 27 | @clock = SFML.sfClock_create 28 | @vec2u = SFML::Vector2u.new 29 | @vec2f = SFML::Vector2f.new 30 | @view = SFML.sfView_create 31 | 32 | on_resize(TV_WIDTH, HEIGHT) 33 | 34 | @palette = @palette_rgb.map do |r, g, b| 35 | 0xff000000 | (b << 16) | (g << 8) | r 36 | end 37 | end 38 | 39 | def change_window_size(scale) 40 | if scale 41 | @vec2u[:x] = TV_WIDTH * scale 42 | @vec2u[:y] = HEIGHT * scale 43 | SFML.sfRenderWindow_setSize(@window, @vec2u) 44 | end 45 | end 46 | 47 | def on_resize(w, h) 48 | @vec2f[:x] = WIDTH / 2 49 | @vec2f[:y] = HEIGHT / 2 50 | SFML.sfView_setCenter(@view, @vec2f) 51 | 52 | ratio = w.to_f * WIDTH / TV_WIDTH / h 53 | if WIDTH < ratio * HEIGHT 54 | @vec2f[:x] = HEIGHT * ratio 55 | @vec2f[:y] = HEIGHT 56 | else 57 | @vec2f[:x] = WIDTH 58 | @vec2f[:y] = WIDTH / ratio 59 | end 60 | SFML.sfView_setSize(@view, @vec2f) 61 | 62 | SFML.sfRenderWindow_setView(@window, @view) 63 | end 64 | 65 | attr_reader :window 66 | 67 | def tick(colors) 68 | if SFML.sfClock_getElapsedTime(@clock) >= 1_000_000 69 | @fps = @frame 70 | @frame = 0 71 | SFML.sfClock_restart(@clock) 72 | end 73 | @frame += 1 74 | 75 | Driver.cutoff_overscan(colors) 76 | Driver.show_fps(colors, @fps, @palette) if @conf.show_fps 77 | @buf.write_array_of_uint32(colors) 78 | SFML.sfTexture_updateFromPixels(@texture, @buf, WIDTH, HEIGHT, 0, 0) 79 | SFML.sfRenderWindow_drawSprite(@window, @sprite, nil) 80 | SFML.sfRenderWindow_display(@window) 81 | @fps 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/optcarrot/driver/sixel_video.rb: -------------------------------------------------------------------------------- 1 | require_relative "misc" 2 | 3 | module Optcarrot 4 | # Video output driver for Sixel (this is a joke feature) 5 | class SixelVideo < Video 6 | def init 7 | super 8 | @buff = "".b 9 | @line = "".b 10 | @seq_setup = "\e[H\eP7q" 11 | print "\e[2J" 12 | 13 | @palette, colors = Driver.quantize_colors(@palette_rgb) 14 | 15 | colors.each_with_index do |rgb, c| 16 | @seq_setup << "#" << [c, 2, *rgb.map {|clr| clr * 100 / 255 }].join(";") 17 | end 18 | @seq_clr = (0..255).map {|c| "##{ c }" } 19 | @seq_len = (0..256).map {|i| "!#{ i }" } 20 | @seq_len[1] = "" 21 | @seq_end = "\e\\" 22 | end 23 | 24 | def tick(screen) 25 | @buff.replace(@seq_setup) 26 | 40.times do |y| 27 | offset = y * 0x600 28 | six_lines = screen[offset, 0x600] 29 | six_lines.uniq.each do |c| 30 | prev_clr = nil 31 | len = 1 32 | 256.times do |i| 33 | clr = 34 | (six_lines[i] == c ? 0x01 : 0) + 35 | (six_lines[i + 0x100] == c ? 0x02 : 0) + 36 | (six_lines[i + 0x200] == c ? 0x04 : 0) + 37 | (six_lines[i + 0x300] == c ? 0x08 : 0) + 38 | (six_lines[i + 0x400] == c ? 0x10 : 0) + 39 | (six_lines[i + 0x500] == c ? 0x20 : 0) + 63 40 | if prev_clr == clr 41 | len += 1 42 | elsif prev_clr 43 | case len 44 | when 1 then @line << prev_clr 45 | when 2 then @line << prev_clr << prev_clr 46 | else @line << @seq_len[len] << prev_clr 47 | end 48 | len = 1 49 | end 50 | prev_clr = clr 51 | end 52 | if prev_clr != 63 || len != 256 53 | @buff << @seq_clr[c] << @line << @seq_len[len] << prev_clr << 36 # $ 54 | @line.clear 55 | end 56 | end 57 | @buff << 45 # - 58 | end 59 | print @buff << @seq_end 60 | super 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/optcarrot/driver/term_input.rb: -------------------------------------------------------------------------------- 1 | require "io/console" 2 | require "io/wait" 3 | 4 | module Optcarrot 5 | # Input driver for terminal (this is a joke feature) 6 | class TermInput < Input 7 | def init 8 | $stdin.raw! 9 | $stdin.getc if $stdin.ready? 10 | @escape = false 11 | @ticks = { start: 0, select: 0, a: 0, b: 0, right: 0, left: 0, down: 0, up: 0 } 12 | end 13 | 14 | def dispose 15 | $stdin.cooked! 16 | end 17 | 18 | def keydown(pads, code, frame) 19 | event(pads, :keydown, code, 0) 20 | @ticks[code] = frame 21 | end 22 | 23 | def tick(frame, pads) 24 | while $stdin.ready? 25 | ch = $stdin.getbyte 26 | if @escape 27 | @escape = false 28 | case ch 29 | when 0x5b then @escape = true 30 | when 0x41 then keydown(pads, :up, frame) 31 | when 0x42 then keydown(pads, :down, frame) 32 | when 0x43 then keydown(pads, :right, frame) 33 | when 0x44 then keydown(pads, :left, frame) 34 | end 35 | else 36 | case ch 37 | when 0x1b then @escape = true 38 | when 0x58, 0x78 then keydown(pads, :a, frame) 39 | when 0x5a, 0x7a then keydown(pads, :b, frame) 40 | when 0x0d then keydown(pads, :select, frame) 41 | when 0x20 then keydown(pads, :start, frame) 42 | when 0x51, 0x71 then exit 43 | end 44 | end 45 | end 46 | 47 | @ticks.each do |code, prev_frame| 48 | event(pads, :keyup, code, 0) if prev_frame + 5 < frame 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/optcarrot/driver/wav_audio.rb: -------------------------------------------------------------------------------- 1 | module Optcarrot 2 | # Audio output driver saving a WAV file 3 | class WAVAudio < Audio 4 | def init 5 | @buff = [] 6 | end 7 | 8 | def dispose 9 | buff = @buff.pack(@pack_format) 10 | wav = [ 11 | "RIFF", 44 + buff.bytesize, "WAVE", "fmt ", 16, 1, 1, 12 | @rate, @rate * @bits / 8, @bits / 8, @bits, "data", buff.bytesize, buff 13 | ].pack("A4VA4A4VvvVVvvA4VA*") 14 | File.binwrite("audio.wav", wav) 15 | end 16 | 17 | def tick(output) 18 | @buff.concat output 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/optcarrot/mapper/cnrom.rb: -------------------------------------------------------------------------------- 1 | module Optcarrot 2 | # CNROM mapper: http://wiki.nesdev.com/w/index.php/CNROM 3 | class CNROM < ROM 4 | MAPPER_DB[0x03] = self 5 | 6 | def reset 7 | @cpu.add_mappings(0x8000..0xffff, @prg_ref, @chr_ram ? nil : method(:poke_8000)) 8 | end 9 | 10 | def poke_8000(_addr, data) 11 | @chr_ref.replace(@chr_banks[data & 3]) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/optcarrot/mapper/mmc1.rb: -------------------------------------------------------------------------------- 1 | module Optcarrot 2 | # MMC1 mapper: http://wiki.nesdev.com/w/index.php/MMC1 3 | class MMC1 < ROM 4 | MAPPER_DB[0x01] = self 5 | 6 | NMT_MODE = [:first, :second, :vertical, :horizontal] 7 | PRG_MODE = [:conseq, :conseq, :fix_first, :fix_last] 8 | CHR_MODE = [:conseq, :noconseq] 9 | 10 | def init 11 | @nmt_mode = @prg_mode = @chr_mode = nil 12 | @prg_bank = @chr_bank_0 = @chr_bank_1 = 0 13 | end 14 | 15 | def reset 16 | @shift = @shift_count = 0 17 | 18 | @chr_banks = @chr_banks.flatten.each_slice(0x1000).to_a 19 | 20 | @wrk_readable = @wrk_writable = true 21 | @cpu.add_mappings(0x6000..0x7fff, method(:peek_6000), method(:poke_6000)) 22 | @cpu.add_mappings(0x8000..0xffff, @prg_ref, method(:poke_prg)) 23 | 24 | update_nmt(:horizontal) 25 | update_prg(:fix_last, 0, 0) 26 | update_chr(:conseq, 0, 0) 27 | end 28 | 29 | def poke_prg(addr, val) 30 | if val[7] == 1 31 | @shift = @shift_count = 0 32 | else 33 | @shift |= val[0] << @shift_count 34 | @shift_count += 1 35 | if @shift_count == 0x05 36 | case (addr >> 13) & 0x3 37 | when 0 # control 38 | nmt_mode = NMT_MODE[@shift & 3] 39 | prg_mode = PRG_MODE[@shift >> 2 & 3] 40 | chr_mode = CHR_MODE[@shift >> 4 & 1] 41 | update_nmt(nmt_mode) 42 | update_prg(prg_mode, @prg_bank, @chr_bank_0) 43 | update_chr(chr_mode, @chr_bank_0, @chr_bank_1) 44 | when 1 # change chr_bank_0 45 | # update_prg might modify @chr_bank_0 and prevent updating chr bank, 46 | # so keep current value. 47 | bak_chr_bank_0 = @chr_bank_0 48 | update_prg(@prg_mode, @prg_bank, @shift) 49 | @chr_bank_0 = bak_chr_bank_0 50 | update_chr(@chr_mode, @shift, @chr_bank_1) 51 | when 2 # change chr_bank_1 52 | update_chr(@chr_mode, @chr_bank_0, @shift) 53 | when 3 # change png_bank 54 | update_prg(@prg_mode, @shift, @chr_bank_0) 55 | end 56 | @shift = @shift_count = 0 57 | end 58 | end 59 | end 60 | 61 | def update_nmt(nmt_mode) 62 | return if @nmt_mode == nmt_mode 63 | @nmt_mode = nmt_mode 64 | @ppu.nametables = @nmt_mode 65 | end 66 | 67 | def update_prg(prg_mode, prg_bank, chr_bank_0) 68 | return if prg_mode == @prg_mode && prg_bank == @prg_bank && chr_bank_0 == @chr_bank_0 69 | @prg_mode, @prg_bank, @chr_bank_0 = prg_mode, prg_bank, chr_bank_0 70 | 71 | high_bit = chr_bank_0 & (0x10 & (@prg_banks.size - 1)) 72 | prg_bank_ex = ((@prg_bank & 0x0f) | high_bit) & (@prg_banks.size - 1) 73 | case @prg_mode 74 | when :conseq 75 | lower = prg_bank_ex & ~1 76 | upper = lower + 1 77 | when :fix_first 78 | lower = 0 79 | upper = prg_bank_ex 80 | when :fix_last 81 | lower = prg_bank_ex 82 | upper = ((@prg_banks.size - 1) & 0x0f) | high_bit 83 | end 84 | @prg_ref[0x8000, 0x4000] = @prg_banks[lower] 85 | @prg_ref[0xc000, 0x4000] = @prg_banks[upper] 86 | end 87 | 88 | def update_chr(chr_mode, chr_bank_0, chr_bank_1) 89 | return if chr_mode == @chr_mode && chr_bank_0 == @chr_bank_0 && chr_bank_1 == @chr_bank_1 90 | @chr_mode, @chr_bank_0, @chr_bank_1 = chr_mode, chr_bank_0, chr_bank_1 91 | return if @chr_ram 92 | 93 | @ppu.update(0) 94 | if @chr_mode == :conseq 95 | lower = @chr_bank_0 & 0x1e 96 | upper = lower + 1 97 | else 98 | lower = @chr_bank_0 99 | upper = @chr_bank_1 100 | end 101 | @chr_ref[0x0000, 0x1000] = @chr_banks[lower] 102 | @chr_ref[0x1000, 0x1000] = @chr_banks[upper] 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /lib/optcarrot/mapper/mmc3.rb: -------------------------------------------------------------------------------- 1 | module Optcarrot 2 | # MMC3 mapper: http://wiki.nesdev.com/w/index.php/MMC3 3 | class MMC3 < ROM 4 | MAPPER_DB[0x04] = self 5 | 6 | def init(rev = :B) # rev = :A or :B or :C 7 | @persistant = rev != :A 8 | 9 | @prg_banks = @prg_banks.flatten.each_slice(0x2000).to_a 10 | @prg_bank_swap = false 11 | 12 | @chr_banks = @chr_banks.flatten.each_slice(0x0400).to_a 13 | @chr_bank_mapping = [nil] * 8 14 | @chr_bank_swap = false 15 | end 16 | 17 | def reset 18 | @wrk_readable = true 19 | @wrk_writable = false 20 | 21 | poke_a000 = @mirroring != :FourScreen ? method(:poke_a000) : nil 22 | @cpu.add_mappings(0x6000..0x7fff, method(:peek_6000), method(:poke_6000)) 23 | @cpu.add_mappings(0x8000.step(0x9fff, 2), @prg_ref, method(:poke_8000)) 24 | @cpu.add_mappings(0x8001.step(0x9fff, 2), @prg_ref, method(:poke_8001)) 25 | @cpu.add_mappings(0xa000.step(0xbfff, 2), @prg_ref, poke_a000) 26 | @cpu.add_mappings(0xa001.step(0xbfff, 2), @prg_ref, method(:poke_a001)) 27 | @cpu.add_mappings(0xc000.step(0xdfff, 2), @prg_ref, method(:poke_c000)) 28 | @cpu.add_mappings(0xc001.step(0xdfff, 2), @prg_ref, method(:poke_c001)) 29 | @cpu.add_mappings(0xe000.step(0xffff, 2), @prg_ref, method(:poke_e000)) 30 | @cpu.add_mappings(0xe001.step(0xffff, 2), @prg_ref, method(:poke_e001)) 31 | 32 | update_prg(0x8000, 0) 33 | update_prg(0xa000, 1) 34 | update_prg(0xc000, -2) 35 | update_prg(0xe000, -1) 36 | 8.times {|i| update_chr(i * 0x400, i) } 37 | 38 | @clock = 0 39 | @hold = PPU::RP2C02_CC * 16 40 | @ppu.monitor_a12_rising_edge(self) 41 | @cpu.ppu_sync = true 42 | 43 | @count = 0 44 | @latch = 0 45 | @reload = false 46 | @enabled = false 47 | end 48 | 49 | # prg_bank_swap = F T 50 | # 0x8000..0x9fff: 0 2 51 | # 0xa000..0xbfff: 1 1 52 | # 0xc000..0xdfff: 2 0 53 | # 0xe000..0xffff: 3 3 54 | def update_prg(addr, bank) 55 | bank %= @prg_banks.size 56 | addr ^= 0x4000 if @prg_bank_swap && addr[13] == 0 57 | @prg_ref[addr, 0x2000] = @prg_banks[bank] 58 | end 59 | 60 | def update_chr(addr, bank) 61 | return if @chr_ram 62 | idx = addr / 0x400 63 | bank %= @chr_banks.size 64 | return if @chr_bank_mapping[idx] == bank 65 | addr ^= 0x1000 if @chr_bank_swap 66 | @ppu.update(0) 67 | @chr_ref[addr, 0x400] = @chr_banks[bank] 68 | @chr_bank_mapping[idx] = bank 69 | end 70 | 71 | def poke_8000(_addr, data) 72 | @reg_select = data & 7 73 | prg_bank_swap = data[6] == 1 74 | chr_bank_swap = data[7] == 1 75 | 76 | if prg_bank_swap != @prg_bank_swap 77 | @prg_bank_swap = prg_bank_swap 78 | @prg_ref[0x8000, 0x2000], @prg_ref[0xc000, 0x2000] = @prg_ref[0xc000, 0x2000], @prg_ref[0x8000, 0x2000] 79 | end 80 | 81 | if chr_bank_swap != @chr_bank_swap 82 | @chr_bank_swap = chr_bank_swap 83 | unless @chr_ram 84 | @ppu.update(0) 85 | @chr_ref.rotate!(0x1000) 86 | @chr_bank_mapping.rotate!(4) 87 | end 88 | end 89 | end 90 | 91 | def poke_8001(_addr, data) 92 | if @reg_select < 6 93 | if @reg_select < 2 94 | update_chr(@reg_select * 0x0800, data & 0xfe) 95 | update_chr(@reg_select * 0x0800 + 0x0400, data | 0x01) 96 | else 97 | update_chr((@reg_select - 2) * 0x0400 + 0x1000, data) 98 | end 99 | else 100 | update_prg((@reg_select - 6) * 0x2000 + 0x8000, data & 0x3f) 101 | end 102 | end 103 | 104 | def poke_a000(_addr, data) 105 | @ppu.nametables = data[0] == 1 ? :horizontal : :vertical 106 | end 107 | 108 | def poke_a001(_addr, data) 109 | @wrk_readable = data[7] == 1 110 | @wrk_writable = data[6] == 0 && @wrk_readable 111 | end 112 | 113 | def poke_c000(_addr, data) 114 | @ppu.update(0) 115 | @latch = data 116 | end 117 | 118 | def poke_c001(_addr, _data) 119 | @ppu.update(0) 120 | @reload = true 121 | end 122 | 123 | def poke_e000(_addr, _data) 124 | @ppu.update(0) 125 | @enabled = false 126 | @cpu.clear_irq(CPU::IRQ_EXT) 127 | end 128 | 129 | def poke_e001(_addr, _data) 130 | @ppu.update(0) 131 | @enabled = true 132 | end 133 | 134 | def vsync 135 | @clock = @clock > @cpu.next_frame_clock ? @clock - @cpu.next_frame_clock : 0 136 | end 137 | 138 | def a12_signaled(cycle) 139 | clk, @clock = @clock, cycle + @hold 140 | return if cycle < clk 141 | flag = @persistant || @count > 0 142 | if @reload 143 | @reload = false 144 | @count = @latch 145 | elsif @count == 0 146 | @count = @latch 147 | else 148 | @count -= 1 149 | end 150 | @cpu.do_irq(CPU::IRQ_EXT, cycle) if flag && @count == 0 && @enabled 151 | end 152 | end 153 | end 154 | -------------------------------------------------------------------------------- /lib/optcarrot/mapper/uxrom.rb: -------------------------------------------------------------------------------- 1 | module Optcarrot 2 | # UxROM mapper: http://wiki.nesdev.com/w/index.php/UxROM 3 | class UxROM < ROM 4 | MAPPER_DB[0x02] = self 5 | 6 | def reset 7 | @cpu.add_mappings(0x8000..0xffff, @prg_ref, method(:poke_8000)) 8 | end 9 | 10 | def poke_8000(_addr, data) 11 | @prg_ref[0x8000, 0x4000] = @prg_banks[data & 7] 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/optcarrot/nes.rb: -------------------------------------------------------------------------------- 1 | module Optcarrot 2 | FOREVER_CLOCK = 0xffffffff 3 | RP2A03_CC = 12 4 | 5 | # NES emulation main 6 | class NES 7 | FPS = 60 8 | 9 | def initialize(conf = ARGV) 10 | @conf = Config.new(conf) 11 | 12 | @video, @audio, @input = Driver.load(@conf) 13 | 14 | @cpu = CPU.new(@conf) 15 | @apu = @cpu.apu = APU.new(@conf, @cpu, *@audio.spec) 16 | @ppu = @cpu.ppu = PPU.new(@conf, @cpu, @video.palette) 17 | @rom = ROM.load(@conf, @cpu, @ppu) 18 | @pads = Pads.new(@conf, @cpu, @apu) 19 | 20 | @frame = 0 21 | @frame_target = @conf.frames == 0 ? nil : @conf.frames 22 | @fps_history = [] if save_fps_history? 23 | end 24 | 25 | def inspect 26 | "#<#{ self.class }>" 27 | end 28 | 29 | attr_reader :fps, :video, :audio, :input, :cpu, :ppu, :apu 30 | 31 | def reset 32 | @cpu.reset 33 | @apu.reset 34 | @ppu.reset 35 | @rom.reset 36 | @pads.reset 37 | @cpu.boot 38 | @rom.load_battery 39 | end 40 | 41 | def step 42 | @ppu.setup_frame 43 | @cpu.run 44 | @ppu.vsync 45 | @apu.vsync 46 | @cpu.vsync 47 | @rom.vsync 48 | 49 | @input.tick(@frame, @pads) 50 | @fps = @video.tick(@ppu.output_pixels) 51 | @fps_history << @fps if save_fps_history? 52 | @audio.tick(@apu.output) 53 | 54 | @frame += 1 55 | @conf.info("frame #{ @frame }") if @conf.loglevel >= 2 56 | end 57 | 58 | def dispose 59 | if @fps 60 | @conf.info("fps: %.2f (in the last 10 frames)" % @fps) 61 | if @conf.print_fps_history 62 | puts "frame,fps-history" 63 | @fps_history.each_with_index {|fps, frame| puts "#{ frame },#{ fps }" } 64 | end 65 | if @conf.print_p95fps 66 | puts "p95 fps: #{ @fps_history.sort[(@fps_history.length * 0.05).floor] }" 67 | end 68 | puts "fps: #{ @fps }" if @conf.print_fps 69 | end 70 | if @conf.print_video_checksum && @video.instance_of?(Video) 71 | puts "checksum: #{ @ppu.output_pixels.pack("C*").sum }" 72 | end 73 | @video.dispose 74 | @audio.dispose 75 | @input.dispose 76 | @rom.save_battery 77 | @ppu.dispose 78 | end 79 | 80 | def run 81 | reset 82 | 83 | if @conf.stackprof_mode 84 | require "stackprof" 85 | out = @conf.stackprof_output.sub("MODE", @conf.stackprof_mode) 86 | StackProf.start(mode: @conf.stackprof_mode.to_sym, out: out, raw: true) 87 | end 88 | 89 | step until @frame == @frame_target 90 | 91 | if @conf.stackprof_mode 92 | StackProf.stop 93 | StackProf.results 94 | end 95 | ensure 96 | dispose 97 | end 98 | 99 | private 100 | 101 | def save_fps_history? 102 | @conf.print_fps_history || @conf.print_p95fps 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /lib/optcarrot/opt.rb: -------------------------------------------------------------------------------- 1 | module Optcarrot 2 | # dirty methods manipulating and generating methods... 3 | module CodeOptimizationHelper 4 | def initialize(loglevel, enabled_opts) 5 | @loglevel = loglevel 6 | options = self.class::OPTIONS 7 | opts = {} 8 | enabled_opts ||= [:all] 9 | default = 10 | (enabled_opts == [:all] || enabled_opts != [] && enabled_opts.all? {|opt| opt.to_s.start_with?("-") }) 11 | options.each {|opt| opts[opt] = default } 12 | (enabled_opts - [:none, :all]).each do |opt| 13 | val = true 14 | if opt.to_s.start_with?("-") 15 | opt = opt.to_s[1..-1].to_sym 16 | val = false 17 | end 18 | raise "unknown optimization: `#{ opt }'" unless options.include?(opt) 19 | opts[opt] = val 20 | end 21 | options.each {|opt| instance_variable_set(:"@#{ opt }", opts[opt]) } 22 | end 23 | 24 | def depends(opt, depended_opt) 25 | if instance_variable_get(:"@#{ opt }") && !instance_variable_get(:"@#{ depended_opt }") 26 | raise "`#{ opt }' depends upon `#{ depended_opt }'" 27 | end 28 | end 29 | 30 | def gen(*codes) 31 | codes.map {|code| code.to_s.chomp }.join("\n") + "\n" 32 | end 33 | 34 | # change indent 35 | def indent(i, code) 36 | if i > 0 37 | code.gsub(/^(.+)$/) { " " * i + $1 } 38 | elsif i < 0 39 | code.gsub(/^ {#{ -i }}/, "") 40 | else 41 | code 42 | end 43 | end 44 | 45 | # generate a branch 46 | def branch(cond, code1, code2) 47 | gen( 48 | "if #{ cond }", 49 | indent(2, code1), 50 | "else", 51 | indent(2, code2), 52 | "end", 53 | ) 54 | end 55 | 56 | MethodDef = Struct.new(:params, :body) 57 | 58 | METHOD_DEFINITIONS_RE = / 59 | ^(\ +)def\s+(\w+)(?:\((.*)\))?\n 60 | ^((?:\1\ +.*\n|\n)*) 61 | ^\1end$ 62 | /x 63 | # extract all method definitions 64 | def parse_method_definitions(file) 65 | src = File.read(file) 66 | mdefs = {} 67 | src.scan(METHOD_DEFINITIONS_RE) do |indent, meth, params, body| 68 | body = indent(-indent.size - 2, body) 69 | 70 | # noramlize: break `when ... then` 71 | body = body.gsub(/^( *)when +(.*?) +then +(.*)/) { $1 + "when #{ $2 }\n" + $1 + " " + $3 } 72 | 73 | # normalize: return unless 74 | body = "if " + $1 + indent(2, $') + "end\n" if body =~ /\Areturn unless (.*)/ 75 | 76 | # normalize: if modifier -> if statement 77 | nil while body.gsub!(/^( *)((?!#)\S.*) ((?:if|unless) .*\n)/) { indent($1.size, gen($3, " " + $2, "end")) } 78 | 79 | mdefs[meth.to_sym] = MethodDef[params ? params.split(", ") : nil, body] 80 | end 81 | mdefs 82 | end 83 | 84 | # inline method calls with no arguments 85 | def expand_methods(code, mdefs, meths = mdefs.keys) 86 | code.gsub(/^( *)\b(#{ meths * "|" })\b(?:\((.*?)\))?\n/) do 87 | indent, meth, args = $1, $2, $3 88 | body = mdefs[meth.to_sym] 89 | body = body.body if body.is_a?(MethodDef) 90 | if args 91 | mdefs[meth.to_sym].params.zip(args.split(", ")) do |param, arg| 92 | body = replace_var(body, param, arg) 93 | end 94 | end 95 | indent(indent.size, body) 96 | end 97 | end 98 | 99 | def expand_inline_methods(code, meth, mdef) 100 | code.gsub(/\b#{ meth }\b(?:\(((?:@?\w+, )*@?\w+)\))?/) do 101 | args = $1 102 | b = "(#{ mdef.body.chomp.gsub(/ *#.*/, "").gsub("\n", "; ") })" 103 | if args 104 | mdef.params.zip(args.split(", ")) do |param, arg| 105 | b = replace_var(b, param, arg) 106 | end 107 | end 108 | b 109 | end 110 | end 111 | 112 | def replace_var(code, var, bool) 113 | re = var.start_with?("@") ? /#{ var }\b/ : /\b#{ var }\b/ 114 | code.gsub(re) { bool } 115 | end 116 | 117 | def replace_cond_var(code, var, bool) 118 | code.gsub(/(if|unless)\s#{ var }\b/) { $1 + " " + bool } 119 | end 120 | 121 | TRIVIAL_BRANCH_RE = / 122 | ^(\ *)(if|unless)\ (true|false)\n 123 | ^((?:\1\ +.*\n|\n)*) 124 | (?: 125 | \1else\n 126 | ((?:\1\ +.*\n|\n)*) 127 | )? 128 | ^\1end\n 129 | /x 130 | # remove "if true" or "if false" 131 | def remove_trivial_branches(code) 132 | code = code.dup 133 | nil while 134 | code.gsub!(TRIVIAL_BRANCH_RE) do 135 | if ($2 == "if") == ($3 == "true") 136 | indent(-2, $4) 137 | else 138 | $5 ? indent(-2, $5) : "" 139 | end 140 | end 141 | code 142 | end 143 | 144 | # replace instance variables with temporal local variables 145 | # CAUTION: the instance variable must not be accessed out of CPU#run 146 | def localize_instance_variables(code, ivars = code.scan(/@\w+/).uniq.sort) 147 | ivars = ivars.map {|ivar| ivar.to_s[1..-1] } 148 | 149 | inits, finals = [], [] 150 | ivars.each do |ivar| 151 | lvar = "__#{ ivar }__" 152 | inits << "#{ lvar } = @#{ ivar }" 153 | finals << "@#{ ivar } = #{ lvar }" 154 | end 155 | 156 | code = code.gsub(/@(#{ ivars * "|" })\b/) { "__#{ $1 }__" } 157 | 158 | gen( 159 | "begin", 160 | indent(2, inits.join("\n")), 161 | indent(2, code), 162 | "ensure", 163 | indent(2, finals.join("\n")), 164 | "end", 165 | ) 166 | end 167 | end 168 | end 169 | -------------------------------------------------------------------------------- /lib/optcarrot/pad.rb: -------------------------------------------------------------------------------- 1 | module Optcarrot 2 | # Pad pair implementation (NES has two built-in game pad.) 3 | class Pads 4 | def inspect 5 | "#<#{ self.class }>" 6 | end 7 | 8 | ########################################################################### 9 | # initialization 10 | 11 | def initialize(conf, cpu, apu) 12 | @conf = conf 13 | @cpu = cpu 14 | @apu = apu 15 | @pads = [Pad.new, Pad.new] 16 | end 17 | 18 | def reset 19 | @cpu.add_mappings(0x4016, method(:peek_401x), method(:poke_4016)) 20 | @cpu.add_mappings(0x4017, method(:peek_401x), @apu.method(:poke_4017)) # delegate 4017H to APU 21 | @pads[0].reset 22 | @pads[1].reset 23 | end 24 | 25 | def peek_401x(addr) 26 | @cpu.update 27 | @pads[addr - 0x4016].peek | 0x40 28 | end 29 | 30 | def poke_4016(_addr, data) 31 | @pads[0].poke(data) 32 | @pads[1].poke(data) 33 | end 34 | 35 | ########################################################################### 36 | # APIs 37 | 38 | def keydown(pad, btn) 39 | @pads[pad].buttons |= 1 << btn 40 | end 41 | 42 | def keyup(pad, btn) 43 | @pads[pad].buttons &= ~(1 << btn) 44 | end 45 | end 46 | 47 | ########################################################################### 48 | # each pad 49 | class Pad 50 | A = 0 51 | B = 1 52 | SELECT = 2 53 | START = 3 54 | UP = 4 55 | DOWN = 5 56 | LEFT = 6 57 | RIGHT = 7 58 | 59 | def initialize 60 | reset 61 | end 62 | 63 | def reset 64 | @strobe = false 65 | @buttons = @stream = 0 66 | end 67 | 68 | def poke(data) 69 | prev = @strobe 70 | @strobe = data[0] == 1 71 | @stream = ((poll_state << 1) ^ -512) if prev && !@strobe 72 | end 73 | 74 | def peek 75 | return poll_state & 1 if @strobe 76 | @stream >>= 1 77 | return @stream[0] 78 | end 79 | 80 | def poll_state 81 | state = @buttons 82 | 83 | # prohibit impossible simultaneous keydown (right and left, up and down) 84 | state &= 0b11001111 if state & 0b00110000 == 0b00110000 85 | state &= 0b00111111 if state & 0b11000000 == 0b11000000 86 | 87 | state 88 | end 89 | 90 | attr_accessor :buttons 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /lib/optcarrot/palette.rb: -------------------------------------------------------------------------------- 1 | module Optcarrot 2 | # NES palette generators 3 | module Palette 4 | module_function 5 | 6 | # I don't know where this palette definition came from, but many emulators are using this palette 7 | def defacto_palette 8 | [ 9 | [1.00, 1.00, 1.00], # default 10 | [1.00, 0.80, 0.81], # emphasize R 11 | [0.78, 0.94, 0.66], # emphasize G 12 | [0.79, 0.77, 0.63], # emphasize RG 13 | [0.82, 0.83, 1.12], # emphasize B 14 | [0.81, 0.71, 0.87], # emphasize RB 15 | [0.68, 0.79, 0.79], # emphasize GB 16 | [0.70, 0.70, 0.70], # emphasize RGB 17 | ].flat_map do |rf, gf, bf| 18 | # RGB default palette (I don't know where this palette came from) 19 | [ 20 | 0x666666, 0x002a88, 0x1412a7, 0x3b00a4, 0x5c007e, 0x6e0040, 0x6c0600, 0x561d00, 21 | 0x333500, 0x0b4800, 0x005200, 0x004f08, 0x00404d, 0x000000, 0x000000, 0x000000, 22 | 0xadadad, 0x155fd9, 0x4240ff, 0x7527fe, 0xa01acc, 0xb71e7b, 0xb53120, 0x994e00, 23 | 0x6b6d00, 0x388700, 0x0c9300, 0x008f32, 0x007c8d, 0x000000, 0x000000, 0x000000, 24 | 0xfffeff, 0x64b0ff, 0x9290ff, 0xc676ff, 0xf36aff, 0xfe6ecc, 0xfe8170, 0xea9e22, 25 | 0xbcbe00, 0x88d800, 0x5ce430, 0x45e082, 0x48cdde, 0x4f4f4f, 0x000000, 0x000000, 26 | 0xfffeff, 0xc0dfff, 0xd3d2ff, 0xe8c8ff, 0xfbc2ff, 0xfec4ea, 0xfeccc5, 0xf7d8a5, 27 | 0xe4e594, 0xcfef96, 0xbdf4ab, 0xb3f3cc, 0xb5ebf2, 0xb8b8b8, 0x000000, 0x000000, 28 | ].map do |rgb| 29 | r = [((rgb >> 16 & 0xff) * rf).floor, 0xff].min 30 | g = [((rgb >> 8 & 0xff) * gf).floor, 0xff].min 31 | b = [((rgb >> 0 & 0xff) * bf).floor, 0xff].min 32 | [r, g, b] 33 | end 34 | end 35 | end 36 | 37 | # Nestopia generates a palette systematically (cool!), but it is not compatible with nes-tests-rom 38 | def nestopia_palette 39 | (0..511).map do |n| 40 | tint, level, color = n >> 6 & 7, n >> 4 & 3, n & 0x0f 41 | level0, level1 = [[-0.12, 0.40], [0.00, 0.68], [0.31, 1.00], [0.72, 1.00]][level] 42 | level0 = level1 if color == 0x00 43 | level1 = level0 if color == 0x0d 44 | level0 = level1 = 0 if color >= 0x0e 45 | y = (level1 + level0) * 0.5 46 | s = (level1 - level0) * 0.5 47 | iq = Complex.polar(s, Math::PI / 6 * (color - 3)) 48 | if tint != 0 && color <= 0x0d 49 | if tint == 7 50 | y = (y * 0.79399 - 0.0782838) * 1.13 51 | else 52 | level1 = (level1 * (1 - 0.79399) + 0.0782838) * 0.5 53 | y -= level1 * 0.5 54 | y -= level1 *= 0.6 if [3, 5, 6].include?(tint) 55 | iq += Complex.polar(level1, Math::PI / 12 * ([0, 6, 10, 8, 2, 4, 0, 0][tint] * 2 - 7)) 56 | end 57 | end 58 | [[105, 0.570], [251, 0.351], [15, 1.015]].map do |angle, gain| 59 | clr = y + (Complex.polar(gain * 2, (angle - 33) * Math::PI / 180) * iq.conjugate).real 60 | [0, (clr * 255).round, 255].sort[1] 61 | end 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/optcarrot/rom.rb: -------------------------------------------------------------------------------- 1 | module Optcarrot 2 | # Cartridge class (with NROM mapper implemented) 3 | class ROM 4 | MAPPER_DB = { 0x00 => self } 5 | 6 | # These are optional 7 | require_relative "mapper/mmc1" 8 | require_relative "mapper/uxrom" 9 | require_relative "mapper/cnrom" 10 | require_relative "mapper/mmc3" 11 | 12 | def self.zip_extract(filename) 13 | require "zlib" 14 | bin = File.binread(filename) 15 | loop do 16 | sig, _, flags, comp, _, _, _, data_len, _, fn_len, ext_len = bin.slice!(0, 30).unpack("a4v5V3v2") 17 | break if sig != "PK\3\4".b 18 | fn = bin.slice!(0, fn_len) 19 | bin.slice!(0, ext_len) 20 | data = bin.slice!(0, data_len) 21 | next if File.extname(fn).downcase != ".nes" 22 | next if flags & 0x11 != 0 23 | next if comp != 0 && comp != 8 24 | if comp == 8 25 | zs = Zlib::Inflate.new(-15) 26 | data = zs.inflate(data) 27 | zs.finish 28 | zs.close 29 | end 30 | return data 31 | end 32 | raise "failed to extract ROM file from `#{ filename }'" 33 | end 34 | 35 | def self.load(conf, cpu, ppu) 36 | filename = conf.romfile 37 | basename = File.basename(filename) 38 | 39 | blob = (File.extname(filename) == ".zip" ? zip_extract(filename) : File.binread(filename)).bytes 40 | 41 | # parse mapper 42 | mapper = (blob[6] >> 4) | (blob[7] & 0xf0) 43 | 44 | klass = MAPPER_DB[mapper] 45 | raise NotImplementedError, "Unsupported mapper type 0x%02x" % mapper unless klass 46 | klass.new(conf, cpu, ppu, basename, blob) 47 | end 48 | 49 | class InvalidROM < StandardError 50 | end 51 | 52 | def parse_header(buf) 53 | raise InvalidROM, "Missing 16-byte header" if buf.size < 16 54 | raise InvalidROM, "Missing 'NES' constant in header" if buf[0, 4] != "NES\x1a".bytes 55 | raise NotImplementedError, "trainer not supported" if buf[6][2] == 1 56 | raise NotImplementedError, "VS cart not supported" if buf[7][0] == 1 57 | raise NotImplementedError, "PAL not supported" unless buf[9][0] == 0 58 | 59 | prg_banks = buf[4] 60 | chr_banks = buf[5] 61 | @mirroring = buf[6][0] == 0 ? :horizontal : :vertical 62 | @mirroring = :four_screen if buf[6][3] == 1 63 | @battery = buf[6][1] == 1 64 | @mapper = (buf[6] >> 4) | (buf[7] & 0xf0) 65 | ram_banks = [1, buf[8]].max 66 | 67 | return prg_banks, chr_banks, ram_banks 68 | end 69 | 70 | def initialize(conf, cpu, ppu, basename, buf) 71 | @conf = conf 72 | @cpu = cpu 73 | @ppu = ppu 74 | @basename = basename 75 | 76 | prg_count, chr_count, wrk_count = parse_header(buf.slice!(0, 16)) 77 | 78 | raise InvalidROM, "EOF in ROM bank data" if buf.size < 0x4000 * prg_count 79 | @prg_banks = (0...prg_count).map { buf.slice!(0, 0x4000) } 80 | 81 | raise InvalidROM, "EOF in CHR bank data" if buf.size < 0x2000 * chr_count 82 | @chr_banks = (0...chr_count).map { buf.slice!(0, 0x2000) } 83 | 84 | @prg_ref = [nil] * 0x10000 85 | @prg_ref[0x8000, 0x4000] = @prg_banks.first 86 | @prg_ref[0xc000, 0x4000] = @prg_banks.last 87 | 88 | @chr_ram = chr_count == 0 # No CHR bank implies CHR-RAM (writable CHR bank) 89 | @chr_ref = @chr_ram ? [0] * 0x2000 : @chr_banks[0].dup 90 | 91 | @wrk_readable = wrk_count > 0 92 | @wrk_writable = false 93 | @wrk = wrk_count > 0 ? (0x6000..0x7fff).map {|addr| addr >> 8 } : nil 94 | 95 | init 96 | 97 | @ppu.nametables = @mirroring 98 | @ppu.set_chr_mem(@chr_ref, @chr_ram) 99 | end 100 | 101 | def init 102 | end 103 | 104 | def reset 105 | @cpu.add_mappings(0x8000..0xffff, @prg_ref, nil) 106 | end 107 | 108 | def inspect 109 | [ 110 | "Mapper: #{ @mapper } (#{ self.class.to_s.split("::").last })", 111 | "PRG Banks: #{ @prg_banks.size }", 112 | "CHR Banks: #{ @chr_banks.size }", 113 | "Mirroring: #{ @mirroring }", 114 | ].join("\n") 115 | end 116 | 117 | def peek_6000(addr) 118 | @wrk_readable ? @wrk[addr - 0x6000] : (addr >> 8) 119 | end 120 | 121 | def poke_6000(addr, data) 122 | @wrk[addr - 0x6000] = data if @wrk_writable 123 | end 124 | 125 | def vsync 126 | end 127 | 128 | def load_battery 129 | return unless @battery 130 | sav = @basename + ".sav" 131 | return unless File.readable?(sav) 132 | sav = File.binread(sav) 133 | @wrk.replace(sav.bytes) 134 | end 135 | 136 | def save_battery 137 | return unless @battery 138 | sav = @basename + ".sav" 139 | puts "Saving: " + sav 140 | File.binwrite(sav, @wrk.pack("C*")) 141 | end 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /optcarrot.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path("lib", __dir__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require "optcarrot" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "optcarrot" 7 | spec.version = Optcarrot::VERSION 8 | spec.authors = ["Yusuke Endoh"] 9 | spec.email = ["mame@ruby-lang.org"] 10 | 11 | spec.summary = "A NES emulator written in Ruby." 12 | spec.description = 13 | 'An "enjoyable" benchmark for Ruby implementation. The goal of this project is to drive Ruby3x3.' 14 | spec.homepage = "https://github.com/mame/optcarrot" 15 | spec.license = "MIT" 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject {|f| f.match(%r{^tmp/|^tools/|^examples/|^\.}) } 18 | spec.bindir = "bin" 19 | spec.executables = ["optcarrot"] 20 | spec.require_paths = ["lib"] 21 | 22 | spec.add_runtime_dependency "ffi", "~> 1.9" 23 | spec.add_development_dependency "bundler", "~> 1.11" 24 | spec.add_development_dependency "rake", "~> 10.0" 25 | spec.add_development_dependency "stackprof", "~> 0.2" 26 | end 27 | -------------------------------------------------------------------------------- /tools/README: -------------------------------------------------------------------------------- 1 | # Welch's t-test for performance improvement 2 | 3 | $ gem install statsample 4 | $ git clone https://github.com/mame/optcarrot.git 5 | $ git clone optcarrot optcarrot.master 6 | $ cd optcarrot 7 | $ vim ... # try to implement optimization 8 | $ ruby tools/statistic-test.rb 9 | 10 | # Detect external method call 11 | 12 | $ optcarrot --dump-cpu | ruby tools/list-method-calls.rb 13 | 14 | # Generate benchmark chart 15 | 16 | $ rm -rf benchmark 17 | $ ruby tools/run-benchmark.rb all -m all 18 | $ cp benchmark/*-oneshot.csv oneshot.csv 19 | 20 | $ rm -rf benchmark 21 | $ ruby tools/run-benchmark.rb ruby27mjit,ruby27,ruby20,truffleruby,jruby,topaz -h 1 -c 10 22 | $ cp benchmark/*-elapsed-time.csv elapsed-time.csv 23 | 24 | $ rm -rf benchmark 25 | $ ruby tools/run-benchmark.rb ruby27mjit,ruby27,ruby20,truffleruby,jruby,topaz -h 3000 26 | $ cp benchmark/*-fps-history-default-1.csv fps-history.csv 27 | 28 | $ ruby tools/plot.rb oneshot.csv elapsed-time.csv fps-history.csv 29 | 30 | # Compile ico 31 | 32 | $ convert tools/optcarrot.ico tools/optcarrot.png 33 | $ ruby tools/compile-ico.rb tools/optcarrot.png 34 | 35 | # Rewrite the whole program for Opal and Ruby 1.8 36 | 37 | $ ruby rewrite.rb 38 | 39 | # Read ROM 40 | 41 | $ gem install arduino_firmata 42 | $ ruby tools/reader.rb 43 | -------------------------------------------------------------------------------- /tools/chart-images.js: -------------------------------------------------------------------------------- 1 | var page = require('webpage').create(); 2 | 3 | function capture(page, url, callback) { 4 | page.onConsoleMessage = callback; 5 | page.open(url, function(status) { }); 6 | } 7 | 8 | capture(page, "http://localhost:4567/", function(msg) { 9 | console.log(msg); 10 | capture(page, "http://localhost:4567/default", function(msg) { 11 | console.log(msg); 12 | page.open("http://localhost:4567/exit", function(status) { 13 | phantom.exit(); 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /tools/compile-ico.rb: -------------------------------------------------------------------------------- 1 | require "chunky_png" 2 | 3 | dat = [] 4 | png = ChunkyPNG::Image.from_file(ARGV[0]) 5 | png.height.times do |y| 6 | png.width.times do |x| 7 | clr = "%02x%02x%02x%02x" % [ 8 | ChunkyPNG::Color.a(png[x, y]), 9 | ChunkyPNG::Color.b(png[x, y]), 10 | ChunkyPNG::Color.g(png[x, y]), 11 | ChunkyPNG::Color.r(png[x, y]), 12 | ] 13 | dat << clr.hex 14 | end 15 | end 16 | 17 | offset = 35 18 | palette = dat.sort.uniq 19 | dat = dat.map {|clr| palette.index(clr) + offset }.pack("C*") 20 | tbl = "" 21 | (palette.size + offset).upto(256) do |c| 22 | count = Hash.new(0) 23 | dat.chars.each_cons(2) {|a| count[a.join] += 1 } 24 | max = count.values.max 25 | break if max == 2 26 | k, = count.find {|_, v| v == max } 27 | tbl = k + tbl 28 | dat = dat.gsub(k, c.chr) 29 | end 30 | 31 | code = DATA.read 32 | code.sub!("PALETTE") { "[#{ palette.map {|clr| "0x%08x" % clr }.join(", ") }]" } 33 | code.sub!("STR") { dat.dump } 34 | code.sub!("NUM") { tbl.size / 2 + palette.size + offset - 1 } 35 | code.sub!("TBL") { tbl.dump } 36 | code.sub!("OFFSET") { offset } 37 | puts code 38 | 39 | __END__ 40 | palette = PALETTE 41 | dat = STR 42 | i = NUM 43 | TBL.scan(/../) do 44 | dat = dat.gsub(i.chr, $&) 45 | i -= 1 46 | end 47 | ICO = dat.bytes.map {|clr| palette[clr - OFFSET] } 48 | -------------------------------------------------------------------------------- /tools/list-method-calls.rb: -------------------------------------------------------------------------------- 1 | require "ripper" 2 | 3 | METHOD_LIST = {} 4 | def recur(type, *args) 5 | if type.is_a?(Array) 6 | recur(*type) unless type.empty? 7 | elsif [:vcall, :fcall, :command_call].include?(type) 8 | METHOD_LIST[args[0][1]] = true 9 | end 10 | args.each do |subtree| 11 | recur(subtree) 12 | end 13 | end 14 | recur(*Ripper.sexp(ARGF.read)) 15 | p METHOD_LIST.keys 16 | -------------------------------------------------------------------------------- /tools/mruby_optcarrot_config.rb: -------------------------------------------------------------------------------- 1 | MRuby::Build.new do |conf| 2 | toolchain :gcc 3 | conf.cc.flags << "-DMRB_WITHOUT_FLOAT" 4 | conf.gem core: "mruby-print" 5 | conf.gem core: "mruby-struct" 6 | conf.gem core: "mruby-string-ext" 7 | conf.gem core: "mruby-hash-ext" 8 | conf.gem core: "mruby-fiber" 9 | conf.gem core: "mruby-enumerator" 10 | conf.gem core: "mruby-bin-mruby" 11 | conf.gem core: "mruby-kernel-ext" 12 | conf.gem core: "mruby-eval" 13 | conf.gem core: "mruby-io" 14 | conf.gem core: "mruby-pack" 15 | conf.gem core: "mruby-metaprog" 16 | conf.gem core: "mruby-exit" 17 | conf.gem mgem: "mruby-gettimeofday" 18 | conf.gem mgem: "mruby-method" 19 | conf.gem mgem: "mruby-regexp-pcre" 20 | end 21 | -------------------------------------------------------------------------------- /tools/plot.rb: -------------------------------------------------------------------------------- 1 | require "csv" 2 | require "pycall/import" 3 | include PyCall::Import 4 | 5 | pyimport "numpy", as: "np" 6 | pyimport "pandas", as: "pd" 7 | pyimport "matplotlib", as: "mpl" 8 | 9 | mpl.use("agg") 10 | 11 | pyimport "matplotlib.pyplot", as: "plt" 12 | pyimport "matplotlib.patches", as: "patches" 13 | pyimport "matplotlib.path", as: "path" 14 | 15 | if ARGV.size < 2 16 | puts "Usage: #$0 benchmark/...oneshot-180.csv benchmark/...oneshot-3000.csv" 17 | end 18 | 19 | [180, 3000].each do |frames| 20 | df = pd.read_csv(frames == 180 ? ARGV[0] : ARGV[1], index_col: ["mode", "name"]) 21 | df = df.filter(regex: "run \\d+").stack().to_frame("fps") 22 | idx = df.index.drop_duplicates 23 | gp = df["fps"].groupby(level: ["mode", "name"]) 24 | [true, false].each do |summary| 25 | mean, std = [gp.mean(), gp.std()].map do |df_| 26 | df_ = df_.unstack("mode") 27 | df_ = df_.reindex(index: idx.get_level_values("name").unique) 28 | df_ = df_.reindex(columns: idx.get_level_values("mode").unique) 29 | df_ = df_["default"].fillna(df_["opt-none"]).to_frame if summary 30 | df_ 31 | end 32 | 33 | d = mean + std 34 | break_start = max = d.max.max.to_f.ceil(-1) + 10 35 | if frames == 3000 36 | d = mean + std 37 | break_start = d[d.index != "truffleruby"].max.max.to_f.ceil(-1) + 10 38 | d = mean - std 39 | break_end = d[d.index == "truffleruby"].min.min.to_f.floor(-1) - 10 40 | end 41 | 42 | gridspec_kw = {} 43 | gridspec_kw[:width_ratios] = [break_start, max - break_end] if frames == 3000 44 | fig, ax0 = plt.subplots( 45 | 1, frames == 180 ? 1 : 2, figsize: [8, frames == 180 ? summary ? 7 : 13 : summary ? 3 : 5], sharey: "col", gridspec_kw: gridspec_kw, 46 | ) 47 | 48 | if frames == 3000 49 | ax1 = ax0[1] 50 | ax0 = ax0[0] 51 | end 52 | 53 | fig.suptitle("Optcarrot, average FPS for frames #{ frames - 9 }-#{ frames }") 54 | fig.patch.set_facecolor("white") 55 | 56 | (frames == 180 ? 1 : 2).times do |i| 57 | mean.plot( 58 | ax: i == 0 ? ax0 : ax1, kind: :barh, width: 0.8, 59 | xerr: std, ecolor: "lightgray", legend: frames == 180 ? !summary : i == 1 && !summary, 60 | ) 61 | end 62 | 63 | fig.subplots_adjust(wspace: 0.0, top: frames == 180 ? summary ? 0.93 : 0.96 : summary ? 0.85 : 0.90) 64 | 65 | if frames == 180 66 | ax0.set_xlim(0, max) 67 | ax0.set_xticks(0.step(max - 10, 10).to_a) 68 | else 69 | ax0.set_xlim(0, break_start) 70 | ax0.set_xticks(0.step(break_start - 10, 10).to_a) 71 | ax1.set_xlim(break_end, max) 72 | ax1.set_xticks((break_end + 10).step(max, 10).to_a) 73 | end 74 | 75 | ax0.set_xlabel("frames per second") 76 | ax0.set_ylabel("") 77 | if frames == 3000 78 | ax0.xaxis.get_label.set_position([(max - break_end + break_start) / 2.0 / break_start, 1]) 79 | ax1.set_ylabel("") 80 | ax0.spines["right"].set_visible(false) 81 | ax1.spines["left"].set_visible(false) 82 | ax1.tick_params(axis: "y", which: "both", left: false, labelleft: false) 83 | ax1.invert_yaxis() 84 | end 85 | ax0.invert_yaxis() 86 | 87 | texts = mean.applymap(->(v) do 88 | v.nan? ? "failure" : "%.#{ (2 - Math.log(v.to_f, 10)).ceil }f" % v 89 | end) 90 | ax0.patches.each_with_index do |rect, i| 91 | x = rect.get_width() + 0.1 92 | y = rect.get_y() + rect.get_height() / 2 93 | n = PyCall.len(mean) 94 | text = texts.iloc[i % n, i / n] 95 | case 96 | when 0 <= x.to_f && x.to_f < break_start 97 | ax0.text(x, y, text, ha: "left", va: "center") 98 | when break_end <= x.to_f && x.to_f < max 99 | ax1.text(x, y, text, ha: "left", va: "center") 100 | end 101 | end 102 | 103 | if frames == 3000 104 | d1 = 0.02 105 | d2 = 0.1 106 | n = 20 107 | ps = (0..n).map do |i| 108 | x = -d1 + (1 + d1 * 2) * i / n 109 | y = [0, 0+d2, 0, 0-d2][i % 4] 110 | [y, x] 111 | end 112 | ps = path.Path.new(ps, [path.Path.MOVETO] + [path.Path.CURVE3] * n) 113 | line1 = patches.PathPatch.new(ps, lw: 4, edgecolor: "black", facecolor: "None", clip_on: false, transform: ax1.transAxes, zorder: 10) 114 | line2 = patches.PathPatch.new(ps, lw: 3, edgecolor: "white", facecolor: "None", clip_on: false, transform: ax1.transAxes, zorder: 10, capstyle: "round") 115 | ax1.add_patch(line1) 116 | ax1.add_patch(line2) 117 | end 118 | 119 | f = frames == 180 ? "" : "-3000" 120 | f = summary ? "doc/benchmark-summary#{ f }.png" : "doc/benchmark-full#{ f }.png" 121 | plt.savefig(f, dpi: 80, bbox_inches: "tight") 122 | plt.close() 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /tools/reader.rb: -------------------------------------------------------------------------------- 1 | # rubocop:disable all 2 | 3 | require "arduino_firmata" 4 | 5 | class ROMReader 6 | D0, D1, D2, D3, D4, D5, D6, D7, D8, D9, D10, D11, D12, D13 = [*0..13] 7 | A0, A1, A2, A3, A4, A5 = [*14..19] 8 | 9 | PIN_DATA_0 = D2 10 | PIN_DATA_1 = D3 11 | PIN_DATA_2 = D4 12 | PIN_DATA_3 = D5 13 | PIN_DATA_4 = D6 14 | PIN_DATA_5 = D7 15 | PIN_DATA_6 = D8 16 | PIN_DATA_7 = D9 17 | 18 | PIN_TS_EN = D10 19 | PIN_TS_DIR = D11 20 | 21 | PIN_FF_CLK = D12 22 | PIN_FF0_EN = A4 23 | PIN_FF1_EN = A5 24 | 25 | PIN_CPU_RW = A0 26 | PIN_PPU_NOT_RD = A1 27 | PIN_NOT_ROMSEL = A2 28 | PIN_M2 = A3 29 | 30 | Mapping = Struct.new(:data, :ff0, :ff1) 31 | PIN_DATA_MAPPING = { 32 | PIN_DATA_0 => { cpu: Mapping[0, 8, 1], ppu: Mapping[4, -13, 8] }, 33 | PIN_DATA_1 => { cpu: Mapping[1, 9, 4], ppu: Mapping[5, 6, 7] }, 34 | PIN_DATA_2 => { cpu: Mapping[2, 14, 0], ppu: Mapping[6, 1, 10] }, 35 | PIN_DATA_3 => { cpu: Mapping[3, 12, 5], ppu: Mapping[3, 3, 9] }, 36 | PIN_DATA_4 => { cpu: Mapping[4, 13, 2], ppu: Mapping[7, 2, 11] }, 37 | PIN_DATA_5 => { cpu: Mapping[5, nil, 3], ppu: Mapping[2, nil, 13] }, 38 | PIN_DATA_6 => { cpu: Mapping[6, 10, 6], ppu: Mapping[1, 5, 0] }, 39 | PIN_DATA_7 => { cpu: Mapping[7, 11, 7], ppu: Mapping[0, 4, 12] }, 40 | } 41 | 42 | Mode = Struct.new(:cpu_rw, :not_romsel, :m2, :ppu_not_rd) 43 | Modes = { 44 | cpu: Mode[true , false, true , true ], 45 | ppu: Mode[false, true , false, false], 46 | } 47 | 48 | def initialize( 49 | mapper: raise, 50 | mirroring: raise, 51 | prg_banks: raise, 52 | chr_banks: raise, 53 | battery: false, 54 | trainer: nil, 55 | vs_unisystem: false, 56 | playchoice_10: false, 57 | tv_system: :ntsc, 58 | wram_banks: 0, 59 | bus_conflicts: false 60 | ) 61 | @prg_banks = prg_banks 62 | @chr_banks = chr_banks 63 | 64 | case mapper 65 | when 0 66 | else 67 | raise "unknown mapper: #{ mapper }" 68 | end 69 | 70 | case mirroring 71 | when :vertical then flags_6 = 0b0001 72 | when :horizontal then flags_6 = 0b0001 73 | when :fourscreen then flags_6 = 0b1000 74 | else 75 | raise "unknown mirroring: #{ mirroring }" 76 | end 77 | flags_6 |= 1 << 1 if battery 78 | flags_6 |= 1 << 2 if trainer 79 | flags_6 |= (mapper & 0x0f) << 4 80 | 81 | flags_7 = 0 82 | flags_7 |= 1 << 0 if vs_unisystem 83 | flags_7 |= 1 << 1 if playchoice_10 84 | flags_7 |= mapper & 0xf0 85 | 86 | case tv_system 87 | when :ntsc then flags_9, flags_10 = 0, 0 88 | when :pal then flags_9, flags_10 = 1, 2 89 | else 90 | raise "unknown TV system: #{ tv_system }" 91 | end 92 | flags_10 |= 1 << 4 if wram_banks > 0 93 | flags_10 |= 1 << 5 if bus_conflicts 94 | 95 | @buffer = [ 96 | "NES\x1a", 97 | prg_banks, 98 | chr_banks, 99 | flags_6, 100 | flags_7, 101 | wram_banks, 102 | flags_9, 103 | flags_10, 104 | 0, 105 | 0, 106 | 0, 107 | 0, 108 | 0, 109 | ].pack("A4C*") 110 | 111 | if trainer 112 | raise if trainer.bytesize != 512 113 | @buffer.concat(trainer) 114 | end 115 | 116 | @ard = ArduinoFirmata.connect 117 | end 118 | 119 | def run 120 | setup 121 | 122 | set_mode(:cpu) 123 | read_rom(0x0000, 0x4000 * @prg_banks) 124 | 125 | set_mode(:ppu) 126 | read_rom(0x0000, 0x2000 * @chr_banks) 127 | 128 | dump 129 | end 130 | 131 | def setup 132 | each_data_pin do |pin, i| 133 | @ard.pin_mode(pin, ArduinoFirmata::OUTPUT) 134 | end 135 | 136 | [ 137 | PIN_TS_EN, PIN_TS_DIR, PIN_FF_CLK, PIN_FF0_EN, PIN_FF1_EN, 138 | PIN_CPU_RW, PIN_PPU_NOT_RD, PIN_NOT_ROMSEL, PIN_M2 139 | ].each do |pin| 140 | @ard.pin_mode(pin, ArduinoFirmata::OUTPUT) 141 | end 142 | 143 | @ard.digital_write(PIN_TS_EN , true) # Disable 144 | @ard.digital_write(PIN_FF0_EN, true) # Disable 145 | @ard.digital_write(PIN_FF1_EN, true) # Disable 146 | @ard.digital_write(PIN_FF_CLK, false) 147 | @ard.digital_write(PIN_TS_DIR, false) # input 148 | 149 | @ard.digital_write(PIN_CPU_RW , false) 150 | @ard.digital_write(PIN_NOT_ROMSEL, false) 151 | @ard.digital_write(PIN_M2 , true) 152 | @ard.digital_write(PIN_PPU_NOT_RD, true) 153 | end 154 | 155 | def set_mode(mode) 156 | @mode = mode 157 | mode = Modes[mode] 158 | 159 | @ard.digital_write(PIN_CPU_RW , mode.cpu_rw) 160 | @ard.digital_write(PIN_NOT_ROMSEL, mode.not_romsel) 161 | @ard.digital_write(PIN_M2 , mode.m2) 162 | @ard.digital_write(PIN_PPU_NOT_RD, mode.ppu_not_rd) 163 | end 164 | 165 | def read_rom(start, len) 166 | start.upto(start + len - 1) do |addr| 167 | # set address 168 | print "%s[%04x]: " % [@mode, addr] 169 | set_addr(PIN_FF0_EN, :ff0, addr) 170 | set_addr(PIN_FF1_EN, :ff1, addr) 171 | 172 | # read data 173 | byte = read_byte 174 | @buffer << byte 175 | puts "%08b" % byte 176 | end 177 | end 178 | 179 | def set_addr(pin_ff_en, idx, addr) 180 | @ard.digital_write(pin_ff_en, false) # flip-flop enable 181 | each_data_pin do |pin| 182 | i = PIN_DATA_MAPPING[pin][@mode][idx] 183 | v = false 184 | if i 185 | v = addr[i.abs] == 1 186 | v = !v if i < 0 187 | end 188 | @ard.digital_write(pin, v) 189 | end 190 | @ard.digital_write(PIN_FF_CLK, true) # latch! 191 | @ard.digital_write(PIN_FF_CLK, false) 192 | @ard.digital_write(pin_ff_en, true) # flip-flop disable 193 | end 194 | 195 | def read_byte 196 | each_data_pin do |pin, _i| 197 | @ard.pin_mode(pin, ArduinoFirmata::INPUT) 198 | end 199 | @ard.digital_write(PIN_TS_EN, false) 200 | sleep 1.0 / 32 201 | byte = 0 202 | each_data_pin do |pin| 203 | byte |= 1 << PIN_DATA_MAPPING[pin][@mode].data if @ard.digital_read(pin) 204 | end 205 | @ard.digital_write(PIN_TS_EN, true) 206 | each_data_pin do |pin, _i| 207 | @ard.pin_mode(pin, ArduinoFirmata::OUTPUT) 208 | end 209 | 210 | byte 211 | end 212 | 213 | def each_data_pin 214 | [ 215 | PIN_DATA_0, PIN_DATA_1, PIN_DATA_2, PIN_DATA_3, 216 | PIN_DATA_4, PIN_DATA_5, PIN_DATA_6, PIN_DATA_7, 217 | ].each_with_index {|pin, i| yield pin, i } 218 | end 219 | 220 | def dump 221 | File.binwrite("tmp.nes", @buffer) 222 | end 223 | end 224 | 225 | conf = { 226 | mapper: 0, 227 | mirroring: :vertical, 228 | prg_banks: 2, 229 | chr_banks: 1, 230 | } 231 | ROMReader.new(conf).run 232 | 233 | __END__ 234 | 235 | A custom "NES ROM Reader" Arduino shield (based on "Hongkong with Arduino") 236 | 237 | Chips: 238 | 239 | * a: Arduino Uno 240 | * b: 74245 (Octal Bus Transceiver) 241 | * f: 74377 (Octal D Flip-flop) 242 | * g: 74377 (Octal D Flip-flop) 243 | * z: Famicom Cartridge 244 | 245 | Pins: 246 | 247 | * a 248 | * D2..D9 (for bus) 249 | * D10..D12, A0..A5 (for control signals) 250 | * GND, 5V 251 | * b: b01..b20 252 | * f: f01..g20 253 | * g: g01..g20 254 | * z: 01..60 255 | 256 | Connections: 257 | 258 | D2 --- b02 --- f03 --- g03 259 | D3 --- b02 --- f18 --- g18 260 | D4 --- b03 --- f04 --- g04 261 | D5 --- b04 --- f17 --- g17 262 | D6 --- b06 --- f07 --- g07 263 | D7 --- b07 --- f08 --- g08 264 | D8 --- b08 --- f14 --- g14 265 | D9 --- b09 --- f13 --- g13 266 | 267 | D10 --- b19 [74245 EN] 268 | D11 --- b01 [74245 DIR] 269 | D12 --- f11 [74377 CLK] --- g11 [74377 CLK] 270 | A0 ---- 14 [CPU R/W] 271 | A1 ---- 17 [PPU /RD] 272 | A2 ---- 44 [/ROMSEL] 273 | A3 ---- 32 [M2] --- 47 [PPU /WR] 274 | A4 ---- g01 [74377 EN] 275 | A5 ---- f01 [74377 EN] 276 | 277 | b18 --- 43 [CPU D0] ---- 60 [PPU D4] 278 | b17 --- 42 [CPU D1] ---- 59 [PPU D5] 279 | b16 --- 41 [CPU D2] ---- 58 [PPU D6] 280 | b15 --- 40 [CPU D3] ---- 29 [PPU D3] 281 | b14 --- 39 [CPU D4] ---- 57 [PPU D7] 282 | b13 --- 38 [CPU D5] ---- 28 [PPU D2] 283 | b12 --- 37 [CPU D6] ---- 27 [PPU D1] 284 | b11 --- 36 [CPU D7] ---- 26 [PPU D0] 285 | 286 | f02 --- 05 [CPU A8] ---- 49 [PPU /A13] 287 | f05 --- 35 [CPU A14] --- 24 [PPU A1] 288 | f06 --- 34 [CPU A13] --- 23 [PPU A2] 289 | f12 --- 02 [CPU A11] --- 21 [PPU A4] 290 | f15 --- 03 [CPU A10] --- 20 [PPU A5] 291 | f16 --- 33 [CPU A12] --- 22 [PPU A3] 292 | f19 --- 04 [CPU A9] ---- 19 [PPU A6] 293 | 294 | g02 --- 12 [CPU A1] ---- 51 [PPU A8] 295 | g05 --- 13 [CPU A0] ---- 53 [PPU A10] 296 | g06 --- 11 [CPU A2] ---- 54 [PPU A11] 297 | g09 --- 10 [CPU A3] ---- 56 [PPU A13] 298 | g12 --- 06 [CPU A7] ---- 55 [PPU A12] 299 | g15 --- 07 [CPU A6] ---- 25 [PPU A0] 300 | g16 --- 08 [CPU A5] ---- 52 [PPU A9] 301 | g19 --- 09 [CPU A4] ---- 50 [PPU A7] 302 | 303 | GND --- b10 --- f10 --- g10 --- 01 304 | 5V --- b20 --- f20 --- g20 --- 30 (--- 31) 305 | -------------------------------------------------------------------------------- /tools/rewrite.rb: -------------------------------------------------------------------------------- 1 | require "ripper" 2 | 3 | # Code rewriter for 1.8/opal compatibility 4 | # foo(1, 2, 3,) => foo(1, 2, 3) 5 | # foo(label: 42) => foo(:label => 42) 6 | # /.../x => (removed) 7 | # dynamic require => (removed) 8 | class Rewriter < Ripper::Filter 9 | def on_default(event, tok, out) 10 | if @comma 11 | case event 12 | when :on_sp, :on_ignored_nl 13 | @comma << tok 14 | return out 15 | end 16 | out << @comma if event != :on_rparen 17 | @comma = nil 18 | end 19 | 20 | case event 21 | when :on_label 22 | out << ":#{ tok[0..-2] } =>" 23 | when :on_comma 24 | @comma = "," 25 | else 26 | out << tok 27 | end 28 | 29 | out 30 | end 31 | end 32 | 33 | Dir[File.join(File.dirname(File.dirname(__FILE__)), "lib/**/*.rb")].each do |f| 34 | s = File.read(f) 35 | s = s.gsub(/^( +)class OptimizedCodeBuilder\n(?:\1 .*\n|\n)*\1end/) do 36 | $1 + "class OptimizedCodeBuilder; OPTIONS = {}; end # disabled for 1.8/opal" 37 | end 38 | s = s.gsub(%r{^( +)[A-Z_]+ = /\n(?:\1 .*\n)*\1/x|^( +)require .*}) do 39 | $&.gsub(/.+/) { "##{ $& } # disable for opal" } 40 | end 41 | out = "" 42 | Rewriter.new(s).parse(out) 43 | File.write(f, out) 44 | end 45 | -------------------------------------------------------------------------------- /tools/run-benchmark.rb: -------------------------------------------------------------------------------- 1 | require "optparse" 2 | require "csv" 3 | 4 | BENCHMARK_DIR = File.join(File.dirname(__dir__), "benchmark") 5 | Dir.mkdir(BENCHMARK_DIR) unless File.exist?(BENCHMARK_DIR) 6 | 7 | # Dockerfile generator + helper methods 8 | class DockerImage 9 | IMAGES = [] 10 | def self.inherited(klass) 11 | IMAGES << klass 12 | super 13 | end 14 | 15 | # default 16 | FROM = "ruby:2.7" 17 | APT = [] 18 | URL = nil 19 | RUN = [] 20 | REWRITE = false 21 | RUBY = "ruby" 22 | CMD = "RUBY -v -Ilib -r ./tools/shim bin/optcarrot --benchmark $OPTIONS" 23 | SUPPORTED_MODE = :any 24 | SLOW = false 25 | 26 | def self.tag 27 | name.to_s.downcase 28 | end 29 | 30 | def self.fast? 31 | !self::SLOW 32 | end 33 | 34 | def self.dockerfile_text 35 | lines = [] 36 | lines << "FROM " + self::FROM 37 | lines << "WORKDIR /root" 38 | apts = [*self::APT] 39 | apts << "wget" << "bzip2" if self::URL 40 | unless apts.empty? 41 | lines << "RUN apt-get update" 42 | lines << "RUN apt-get install -y #{ apts * " " }" 43 | end 44 | if self::URL 45 | lines << "RUN wget -q #{ self::URL }" 46 | lines << "RUN tar xf #{ File.basename(self::URL) }" 47 | end 48 | self::RUN.each do |line| 49 | lines << (line.is_a?(Array) && line[0] == :add ? "ADD #{ line.drop(1).join(" ") }" : "RUN #{ line }") 50 | end 51 | lines << "ADD . ." 52 | lines << "RUN ruby tools/rewrite.rb" if self::REWRITE 53 | lines << "CMD #{ self::CMD.sub("RUBY") { self::RUBY } }" 54 | lines.join("\n") + "\n" 55 | end 56 | 57 | def self.dockerfile_path 58 | File.join(BENCHMARK_DIR, "Dockerfile.#{ tag }") 59 | end 60 | 61 | def self.create_dockerfile 62 | File.write(dockerfile_path, dockerfile_text) 63 | end 64 | 65 | def self.pregenerate 66 | %w(ppu cpu).each do |type| 67 | %w(none all).each do |opt| 68 | out = File.join(BENCHMARK_DIR, "#{ type }-core-opt-#{ opt }.rb") 69 | next if File.readable?(out) 70 | optcarrot = File.join(BENCHMARK_DIR, "../bin/optcarrot") 71 | libpath = File.join(BENCHMARK_DIR, "../lib") 72 | system("ruby", "-I", libpath, optcarrot, "--opt-#{ type }=#{ opt }", "--dump-#{ type }", out: out) 73 | end 74 | end 75 | end 76 | 77 | def self.build 78 | create_dockerfile 79 | pregenerate 80 | system("docker", "build", "-t", tag, "-f", dockerfile_path, File.dirname(BENCHMARK_DIR)) || raise 81 | end 82 | 83 | def self.run(mode, romfile, target_frame: nil, history: false) 84 | if self::SUPPORTED_MODE != :any && !self::SUPPORTED_MODE.include?(mode) 85 | puts "#{ tag } does not support the mode `#{ mode }'" 86 | ((@results ||= {})[mode] ||= []) << nil 87 | return 88 | end 89 | 90 | options = [] 91 | case mode 92 | when "default" 93 | when "opt-none" 94 | options << "--load-ppu=benchmark/ppu-core-opt-none.rb" 95 | options << "--load-cpu=benchmark/cpu-core-opt-none.rb" 96 | when "opt-all" 97 | options << "--load-ppu=benchmark/ppu-core-opt-all.rb" 98 | options << "--load-cpu=benchmark/cpu-core-opt-all.rb" 99 | else 100 | options << mode 101 | end 102 | options << "--frames #{ target_frame }" if target_frame 103 | options << "--print-fps-history" if history 104 | options << romfile 105 | 106 | r, w = IO.pipe 107 | now = Time.now 108 | spawn( 109 | "docker", "run", "--security-opt=seccomp=unconfined", "-e", "OPTIONS=" + options.join(" "), "--rm", tag, out: w 110 | ) 111 | w.close 112 | out = r.read 113 | elapsed = Time.now - now 114 | 115 | ((@elapsed_time ||= {})[mode] ||= []) << elapsed 116 | 117 | ruby_v, *fps_history, fps, checksum = out.lines.map {|line| line.chomp } 118 | if history && !fps_history.empty? 119 | raise "fps history broken: #{ fps_history.first }" unless fps_history.first.start_with?("frame,") 120 | fps_history.shift 121 | ((@fps_histories ||= {})[mode] ||= []) << fps_history.map {|s| s.split(",")[1].to_f } 122 | end 123 | puts ruby_v, fps, checksum 124 | fps = fps[/^fps: (\d+\.\d+)$/, 1] if fps 125 | checksum = checksum[/^checksum: (\d+)$/, 1] if checksum 126 | 127 | if fps && checksum 128 | @ruby_v ||= ruby_v 129 | @checksum ||= checksum 130 | raise "ruby version changed: #{ @ruby_v } -> #{ ruby_v }" if @ruby_v != ruby_v 131 | raise "checksum changed: #{ @checksum } -> #{ checksum }" if @checksum != checksum 132 | ((@results ||= {})[mode] ||= []) << fps.to_f 133 | else 134 | puts "FAILED." 135 | ((@results ||= {})[mode] ||= []) << nil 136 | end 137 | end 138 | 139 | def self.test(cmd = %w(bash)) 140 | system("docker", "run", "--rm", "-ti", tag, *cmd) || raise 141 | end 142 | 143 | def self.result_line(mode) 144 | @results ||= {} 145 | [tag, mode, @ruby_v, @checksum, *@results[mode]] 146 | end 147 | 148 | def self.elapsed_time(mode) 149 | @elapsed_time ||= {} 150 | [tag, mode, @ruby_v, @checksum, *@elapsed_time[mode]] 151 | end 152 | 153 | def self.fps_history(mode, count) 154 | @fps_histories ||= {} 155 | fps_history = (@fps_histories[mode] ||= [])[count] 156 | [tag, *fps_history] 157 | end 158 | end 159 | 160 | ############################################################################### 161 | 162 | # https://github.com/rbenv/ruby-build/wiki 163 | MASTER_APT = %w( 164 | autoconf bison build-essential libssl-dev libyaml-dev libreadline6-dev zlib1g-dev libncurses5-dev libffi-dev libgdbm6 165 | libgdbm-dev libdb-dev git ruby 166 | ) 167 | 168 | class MasterMJIT < DockerImage 169 | FROM = "ubuntu:20.04" 170 | APT = MASTER_APT 171 | RUN = [ 172 | "git clone --depth 1 https://github.com/ruby/ruby.git", 173 | "cd ruby && autoconf", 174 | "cd ruby && ./configure --prefix=`pwd`/local", 175 | "cd ruby && make && make install", 176 | ] 177 | RUBY = "ruby/ruby --jit -Iruby" 178 | end 179 | 180 | class Ruby30MJIT < DockerImage 181 | FROM = "rubylang/ruby:3.0-focal" 182 | RUBY = "ruby --jit" 183 | end 184 | 185 | class Ruby27MJIT < DockerImage 186 | FROM = "ruby:2.7" 187 | RUBY = "ruby --jit" 188 | end 189 | 190 | class Ruby26MJIT < DockerImage 191 | FROM = "ruby:2.6" 192 | RUBY = "ruby --jit" 193 | end 194 | 195 | class MasterYJIT < DockerImage 196 | FROM = "ubuntu:20.04" 197 | APT = MASTER_APT 198 | RUN = [ 199 | "git clone --depth 1 https://github.com/ruby/ruby.git", 200 | "cd ruby && autoconf", 201 | "cd ruby && ./configure --prefix=`pwd`/local", 202 | "cd ruby && make && make install", 203 | ] 204 | RUBY = "ruby/ruby --yjit -Iruby" 205 | end 206 | 207 | class Master < DockerImage 208 | FROM = "ubuntu:20.04" 209 | APT = MASTER_APT 210 | RUN = [ 211 | "git clone --depth 1 https://github.com/ruby/ruby.git", 212 | "cd ruby && autoconf", 213 | "cd ruby && ./configure --prefix=`pwd`/local", 214 | "cd ruby && make && make install", 215 | ] 216 | RUBY = "ruby/ruby -Iruby" 217 | end 218 | 219 | class Ruby30 < DockerImage 220 | FROM = "rubylang/ruby:3.0-focal" 221 | end 222 | 223 | class Ruby27 < DockerImage 224 | FROM = "ruby:2.7" 225 | end 226 | 227 | class Ruby26 < DockerImage 228 | FROM = "ruby:2.6" 229 | end 230 | 231 | class Ruby25 < DockerImage 232 | FROM = "ruby:2.5" 233 | end 234 | 235 | class Ruby24 < DockerImage 236 | FROM = "ruby:2.4" 237 | end 238 | 239 | class Ruby23 < DockerImage 240 | FROM = "ruby:2.3" 241 | end 242 | 243 | class Ruby22 < DockerImage 244 | FROM = "ruby:2.2-slim" 245 | end 246 | 247 | class Ruby21 < DockerImage 248 | FROM = "ruby:2.1-slim" 249 | end 250 | 251 | class Ruby20 < DockerImage 252 | FROM = "ruby:2.0-slim" 253 | end 254 | 255 | class Ruby193 < DockerImage 256 | URL = "https://cache.ruby-lang.org/pub/ruby/1.9/ruby-1.9.3-p551.tar.bz2" 257 | RUN = ["cd ruby*/ && ./configure && make ruby"] 258 | RUBY = "ruby*/ruby --disable-gems" 259 | SLOW = true 260 | end 261 | 262 | class Ruby187 < DockerImage 263 | URL = "https://cache.ruby-lang.org/pub/ruby/1.8/ruby-1.8.7-p374.tar.bz2" 264 | RUN = ["cd ruby*/ && ./configure && make ruby"] 265 | REWRITE = true 266 | RUBY = "ruby*/ruby -v -W0 -I ruby*/lib" 267 | SLOW = true 268 | end 269 | 270 | class TruffleRuby < DockerImage 271 | URL = "https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-20.1.0/graalvm-ce-java8-linux-amd64-20.1.0.tar.gz" 272 | FROM = "buildpack-deps:focal" 273 | RUN = ["cd graalvm-* && bin/gu install ruby"] 274 | RUBY = "graalvm-*/bin/ruby --jvm" 275 | SUPPORTED_MODE = %w(default) 276 | end 277 | 278 | class JRuby < DockerImage 279 | FROM = "jruby:9" 280 | RUBY = "jruby -Xcompile.invokedynamic=true" 281 | SLOW = true 282 | end 283 | 284 | class Rubinius < DockerImage 285 | FROM = "rubinius/docker" 286 | SLOW = true 287 | end 288 | 289 | class MRuby < DockerImage 290 | FROM = "buildpack-deps:focal" 291 | APT = %w(bison ruby) 292 | RUN = [ 293 | "git clone --depth 1 https://github.com/mruby/mruby.git", 294 | [:add, "tools/mruby_optcarrot_config.rb", "mruby/"], 295 | "cd mruby && MRUBY_CONFIG=mruby_optcarrot_config.rb ./minirake", 296 | ] 297 | CMD = "mruby/bin/mruby --version && mruby/bin/mruby tools/shim.rb --benchmark $OPTIONS" 298 | SLOW = true 299 | end 300 | 301 | class Topaz < DockerImage 302 | URL = "http://builds.topazruby.com/topaz-linux64-9287c22053d4b2b5f97fa1c65d7d04d5826f9c89.tar.bz2" 303 | RUBY = "topaz/bin/topaz" 304 | end 305 | 306 | class Opal < DockerImage 307 | APT = "nodejs" 308 | RUN = [ 309 | "gem install opal", 310 | ] 311 | REWRITE = true 312 | CMD = "opal -v -I . -r ./tools/shim.rb bin/optcarrot -- --benchmark -f 60 $OPTIONS" 313 | SLOW = true 314 | end 315 | 316 | # class Artichoke < DockerImage 317 | # APT = %w(llvm clang bison ruby) 318 | # FROM = "rustlang/rust:nightly-buster" 319 | # RUN = [ 320 | # "git clone --depth 1 https://github.com/artichoke/artichoke.git", 321 | # "cd artichoke && cargo build --release", 322 | # ] 323 | # CMD = "artichoke/target/release/artichoke -V && " + 324 | # "artichoke/target/release/artichoke bin/optcarrot --benchmark $OPTIONS" 325 | # end 326 | 327 | class RuRuby < DockerImage 328 | FROM = "rustlang/rust:nightly-buster" 329 | RUN = [ 330 | "git clone --depth 1 https://github.com/sisshiki1969/ruruby.git", 331 | "cd ruruby && cargo build --release", 332 | ] 333 | CMD = "git -C ruruby/ rev-parse HEAD && ruruby/target/release/ruruby bin/optcarrot --benchmark $OPTIONS" 334 | end 335 | 336 | ############################################################################### 337 | 338 | # A simple command-line interface 339 | class CLI 340 | def initialize 341 | # default 342 | @mode = "default" 343 | @count = 1 344 | @romfile = "examples/Lan_Master.nes" 345 | @history = nil 346 | 347 | o = OptionParser.new 348 | o.on("-m MODE", "mode (default/opt-none/opt-all/all/each)") {|v| @mode = v } 349 | o.on("-c NUM", Integer, "iteration count") {|v| @count = v } 350 | o.on("-r FILE", String, "rom file") {|v| @romfile = v } 351 | o.on("-f FRAME", Integer, "target frame") {|v| @target_frame = v } 352 | o.on("-h", Integer, "fps history mode") {|v| @history = v } 353 | o.separator("") 354 | o.separator("Examples:") 355 | latest = DockerImage::IMAGES.find {|n| !n.tag.start_with?("master") && !n.tag.include?("mjit") }.tag 356 | o.separator(" ruby tools/run-benchmark.rb #{ latest } -m all " \ 357 | "# run #{ latest } (default mode, opt-none mode, opt-all mode)") 358 | o.separator(" ruby tools/run-benchmark.rb #{ latest } # run #{ latest } (default mode)") 359 | o.separator(" ruby tools/run-benchmark.rb #{ latest } -m opt-none # run #{ latest } (opt-none mode)") 360 | o.separator(" ruby tools/run-benchmark.rb #{ latest } -m opt-all # run #{ latest } (opt-all mode)") 361 | o.separator(" ruby tools/run-benchmark.rb all -m all # run all (default mode)") 362 | o.separator(" ruby tools/run-benchmark.rb all -c 30 -m all # run all (default mode) (30 times for each image)") 363 | o.separator(" ruby tools/run-benchmark.rb not,master,#{ latest } # run all but master and #{ latest }") 364 | o.separator(" ruby tools/run-benchmark.rb #{ latest } bash # custom command") 365 | o.separator(" ruby tools/run-benchmark.rb -r foo.nes #{ latest }") 366 | 367 | @argv = o.parse(ARGV) 368 | 369 | if @argv.empty? 370 | print o.help 371 | exit 372 | end 373 | 374 | @tags = @argv.shift.split(",") 375 | @tags = DockerImage::IMAGES.map {|img| img.tag } if @tags == %w(all) 376 | @tags = DockerImage::IMAGES.map {|img| img.tag if img.fast? }.compact if @tags == %w(fastimpls) 377 | @tags = DockerImage::IMAGES.map {|img| img.tag } - @tags[1..-1] if @tags.first == "not" 378 | end 379 | 380 | def main 381 | if @argv.empty? 382 | run_benchmark 383 | else 384 | run_test 385 | end 386 | end 387 | 388 | def run_benchmark 389 | @timestamp = Time.now.strftime("%Y%m%d%H%M%S") 390 | each_target_image do |img| 391 | banner("build #{ img.tag }") 392 | img.build 393 | end 394 | @count.times do |i| 395 | each_mode do |mode| 396 | each_target_image do |img| 397 | banner("measure #{ img.tag } / #{ mode } (#{ i + 1 } / #{ @count })") 398 | img.run(mode, @romfile, target_frame: @target_frame, history: @history) 399 | save_csv 400 | end 401 | end 402 | end 403 | end 404 | 405 | def run_test 406 | raise "you must specify one tag or test-run" if @tags.size >= 2 407 | each_target_image do |img| 408 | banner("build #{ img.tag }") 409 | img.build 410 | banner("run #{ img.tag }") 411 | img.test(@argv) 412 | end 413 | end 414 | 415 | def each_target_image 416 | DockerImage::IMAGES.each do |img| 417 | next unless @tags.include?(img.tag) 418 | yield img 419 | end 420 | end 421 | 422 | def each_mode 423 | if @mode == "each" 424 | opt_ppu = [] 425 | %w( 426 | none 427 | method_inlining 428 | ivar_localization 429 | split_show_mode 430 | split_a12_checks 431 | fastpath 432 | batch_render_pixels 433 | clock_specialization 434 | ).each do |opt| 435 | opt_ppu << opt 436 | yield "--opt-ppu=#{ opt_ppu.join(",") }" 437 | opt_ppu.clear if opt_ppu == ["none"] 438 | end 439 | 440 | opt_cpu = [] 441 | %w( 442 | none 443 | method_inlining 444 | constant_inlining 445 | ivar_localization 446 | trivial_branches 447 | ).each do |opt| 448 | opt_cpu << opt 449 | yield "--opt-ppu=#{ opt_ppu.join(",") } --opt-cpu=#{ opt_cpu.join(",") }" 450 | opt_cpu.clear if opt_cpu == ["none"] 451 | end 452 | else 453 | %w(default opt-none opt-all).each do |mode| 454 | next unless @mode == mode || @mode == "all" 455 | yield mode 456 | end 457 | end 458 | end 459 | 460 | def banner(msg) 461 | puts "+" + "-" * (msg.size + 2) + "+" 462 | puts "| #{ msg } |" 463 | puts "+" + "-" * (msg.size + 2) + "+" 464 | end 465 | 466 | def save_csv 467 | out = File.join(BENCHMARK_DIR, "#{ @timestamp }-oneshot-#{ @target_frame || 180 }.csv") 468 | CSV.open(out, "w") do |csv| 469 | csv << ["name", "mode", "ruby -v", "checksum", *(1..@count).map {|i| "run #{ i }" }] 470 | each_mode do |mode| 471 | each_target_image do |img| 472 | csv << img.result_line(mode) 473 | end 474 | end 475 | end 476 | 477 | out = File.join(BENCHMARK_DIR, "#{ @timestamp }-elapsed-time-#{ @target_frame || 180 }.csv") 478 | CSV.open(out, "w") do |csv| 479 | csv << ["name", "mode", "ruby -v", "checksum", *(1..@count).map {|i| "run #{ i }" }] 480 | each_mode do |mode| 481 | each_target_image do |img| 482 | csv << img.elapsed_time(mode) 483 | end 484 | end 485 | end 486 | 487 | return unless @history 488 | 489 | each_mode do |mode| 490 | @count.times do |i| 491 | out = File.join(BENCHMARK_DIR, "#{ @timestamp }-fps-history-#{ mode }-#{ i + 1 }.csv") 492 | CSV.open(out, "w") do |csv| 493 | columns = [] 494 | each_target_image do |img| 495 | fps_history = img.fps_history(mode, i) 496 | fps_history << nil until fps_history.size == @history + 1 497 | columns << fps_history 498 | end 499 | columns.unshift(["frame", *(1..@history)]) 500 | columns.transpose.each do |row| 501 | csv << row 502 | end 503 | end 504 | end 505 | end 506 | end 507 | end 508 | 509 | CLI.new.main 510 | -------------------------------------------------------------------------------- /tools/run-tests.rb: -------------------------------------------------------------------------------- 1 | require "digest/sha1" 2 | require "rexml/document" 3 | 4 | TEST_DIR = File.join(__dir__, "nes-test-roms") 5 | unless File.exist?(TEST_DIR) 6 | system("git", "clone", "https://github.com/christopherpow/nes-test-roms.git", TEST_DIR) 7 | system("git", "-C", TEST_DIR, "checkout", "c0cc4cd8937dac4bb6080c82be0fc2e346dc8754") 8 | end 9 | 10 | EXCLUDES = [ 11 | # need work but tentatively... 12 | "other/midscanline.nes", 13 | "scrolltest/scroll.nes", 14 | "mmc3_irq_tests/6.MMC3_rev_B.nes", 15 | 16 | # mappers 0, 1, 2, and 3 are suppored 17 | "exram/mmc5exram.nes", 18 | "nes-test-roms/mmc3_test/6-MMC6.nes", 19 | 20 | # looks pass? 21 | "read_joy3/count_errors.nes", 22 | "read_joy3/count_errors_fast.nes", 23 | 24 | # unsure (no output) 25 | "dmc_tests/buffer_retained.nes", 26 | "dmc_tests/latency.nes", 27 | "dmc_tests/status.nes", 28 | "dmc_tests/status_irq.nes", 29 | 30 | # full palette is not supported yet 31 | "full_palette/flowing_palette.nes", 32 | "full_palette/full_palette.nes", 33 | "full_palette/full_palette_smooth.nes", 34 | "other/blargg_litewall-2.nes", 35 | "scanline/scanline.nes", 36 | "other/litewall5.nes", 37 | "other/RasterDemo.NES", 38 | "other/RasterTest1.NES", 39 | "other/RasterTest2.NES", 40 | "dpcmletterbox/dpcmletterbox.nes", 41 | 42 | # tests that Nestopia fails 43 | "apu_reset/4017_written.nes", 44 | "blargg_ppu_tests_2005.09.15b/power_up_palette.nes", 45 | "cpu_interrupts_v2/cpu_interrupts.nes", 46 | "cpu_interrupts_v2/rom_singles/4-irq_and_dma.nes", 47 | "cpu_interrupts_v2/rom_singles/5-branch_delays_irq.nes", 48 | "ppu_open_bus/ppu_open_bus.nes", 49 | "sprdma_and_dmc_dma/sprdma_and_dmc_dma.nes", 50 | "sprdma_and_dmc_dma/sprdma_and_dmc_dma_512.nes", 51 | "stress/NEStress.NES", 52 | 53 | # tests that Neciside fails (wrong tvsha1?) 54 | "dmc_dma_during_read4/dma_2007_read.nes", 55 | "dmc_dma_during_read4/dma_4016_read.nes", 56 | "oam_stress/oam_stress.nes", 57 | "other/read2004.nes", 58 | ] 59 | 60 | # rubocop:disable Layout/LineLength 61 | SOUND_SHA1 = { 62 | ["apu_mixer/dmc.nes", "dbPq1gWhVJbjPvi61pn/0dUVy/s="] => "7A5a8FmCvRTKu/zqQNodaIqUJR0=", 63 | ["apu_mixer/noise.nes", "eZG7kHcDAzvFUFMXjZynRd3ZyRU="] => "4YaRtnR8eT+V4l4t9/Q4ARPr7sI=", 64 | ["apu_mixer/square.nes", "JXc9txqBccnWpiYoJcNv/D05uCA="] => "yvxKtIzHrSo2BVK29yUHQLP3b64=", 65 | ["apu_mixer/triangle.nes", "CF8XZLs+e9CFTikZ1gHoVjTtWns="] => "sl61rBXsBvu0VhWypk93u6ERerA=", 66 | ["apu_reset/4015_cleared.nes", "75NVOeAT7/jVw73+CEdeKsb2Pic="] => "CykvKb9WfOp6Kwd32RKxBiV1Bdk=", 67 | ["apu_reset/4017_timing.nes", "DDBAM0I78ZhN6S88HzO1gN3WHA8="] => "CykvKb9WfOp6Kwd32RKxBiV1Bdk=", 68 | ["apu_reset/irq_flag_cleared.nes", "75NVOeAT7/jVw73+CEdeKsb2Pic="] => "CykvKb9WfOp6Kwd32RKxBiV1Bdk=", 69 | ["apu_reset/len_ctrs_enabled.nes", "75NVOeAT7/jVw73+CEdeKsb2Pic="] => "AKyS2S0k5hMo8Bj/O44pnJlNGuQ=", 70 | ["apu_reset/works_immediately.nes", "75NVOeAT7/jVw73+CEdeKsb2Pic="] => "YSwNsc5Zzgkbrhpqk/lZMgdcbdM=", 71 | ["apu_test/apu_test.nes", "WbE12eKlTfjwenhtU0Tq70qsaqQ="] => "hEARpWcoV8QegKdqxapnIjcn9TY=", 72 | ["apu_test/rom_singles/1-len_ctr.nes", "1EjN5lks7VxI/HHTIMDfb1GX/lo="] => "StJukmkZ1LFKc38CfxmwPClL79o=", 73 | ["apu_test/rom_singles/2-len_table.nes", "5dFdw9vsWOZg08m95wH7IY5Sry8="] => "4d3rtEiqiGbtkziq+EyGKVFpVdo=", 74 | ["apu_test/rom_singles/3-irq_flag.nes", "bpfq4a8sy8g2F6/RvruaQkcngtM="] => "7RqNzoebK/CYIu5d8MkNWy0n0Jc=", 75 | ["apu_test/rom_singles/4-jitter.nes", "b568KWtuumfzfyQCnq43g0twLAg="] => "ZPP/CEbpJPk3RdJS8j8b9NKJ1fg=", 76 | ["apu_test/rom_singles/5-len_timing.nes", "w+7iZgC2jbZcjILdYvftOC35b+U="] => "b9IkSy142e10izFFHMmrEbIsfm0=", 77 | ["apu_test/rom_singles/6-irq_flag_timing.nes", "Mt3McQrpQOTzXZB4gS0IV0kMqDA="] => "5v9zA2nlCb1zKR/FoRvV3hksSUI=", 78 | ["apu_test/rom_singles/7-dmc_basics.nes", "pBC+8N0h/pcYXTm7k6Bs3rnYf0E="] => "OMuLVW9QGV2ZG574pkLsW67TmEM=", 79 | ["apu_test/rom_singles/8-dmc_rates.nes", "mW8OnTTRl7lokJSVQ8//h5sANzk="] => "u3ttHSALo6lcmleFLkAlx/+0SfM=", 80 | ["blargg_apu_2005.07.30/01.len_ctr.nes", "2ACKiuKHeQth9xxXEZtgRQUIi6w="] => "T+XhxYyM5iG7AAZ2WtW6WnCw6Qg=", 81 | ["blargg_apu_2005.07.30/02.len_table.nes", "2ACKiuKHeQth9xxXEZtgRQUIi6w="] => "izagmEimywCQckeZoQNIfaV10CQ=", 82 | ["blargg_apu_2005.07.30/03.irq_flag.nes", "2ACKiuKHeQth9xxXEZtgRQUIi6w="] => "ixvTINbLedgGHQolX/LL91U9CnU=", 83 | ["blargg_apu_2005.07.30/04.clock_jitter.nes", "2ACKiuKHeQth9xxXEZtgRQUIi6w="] => "jdckH4QcPeIBCRSI5hbtQB4nsl8=", 84 | ["blargg_apu_2005.07.30/05.len_timing_mode0.nes", "2ACKiuKHeQth9xxXEZtgRQUIi6w="] => "qFFfjZSXx/gETae3nIUeqPqrU9o=", 85 | ["blargg_apu_2005.07.30/06.len_timing_mode1.nes", "2ACKiuKHeQth9xxXEZtgRQUIi6w="] => "nljQzO+sZL471oRdddXCfwuP8Tg=", 86 | ["blargg_apu_2005.07.30/07.irq_flag_timing.nes", "2ACKiuKHeQth9xxXEZtgRQUIi6w="] => "JX+rGHLHGbM8UHrF0QCis1STfAg=", 87 | ["blargg_apu_2005.07.30/08.irq_timing.nes", "2ACKiuKHeQth9xxXEZtgRQUIi6w="] => "i0x1uWufTgNNC483Bbbfkl5XMC8=", 88 | ["blargg_apu_2005.07.30/09.reset_timing.nes", "2ACKiuKHeQth9xxXEZtgRQUIi6w="] => "vL8ts7xjPgr9b2NC8XImdKluaXw=", 89 | ["blargg_apu_2005.07.30/10.len_halt_timing.nes", "2ACKiuKHeQth9xxXEZtgRQUIi6w="] => "iJNHM5c027/9aS3rDRpV3prc6DI=", 90 | ["blargg_apu_2005.07.30/11.len_reload_timing.nes", "2ACKiuKHeQth9xxXEZtgRQUIi6w="] => "Mc9zSq/CB54EMtJmBFDP7v9+8eM=", 91 | ["blargg_nes_cpu_test5/cpu.nes", "2/JXgutt9eKd6bBL4vjk1iJ7lpM="] => "yfHW1TAg8tCHCoBCkzHZqfrmIvk=", 92 | ["blargg_nes_cpu_test5/official.nes", "2/JXgutt9eKd6bBL4vjk1iJ7lpM="] => "oOCXQOdX+ekbaMUjDeLKPDwmWuY=", 93 | ["blargg_ppu_tests_2005.09.15b/palette_ram.nes", "2ACKiuKHeQth9xxXEZtgRQUIi6w="] => "DTzrtpk/qotzzyeaPduvd/9bAg4=", 94 | ["blargg_ppu_tests_2005.09.15b/sprite_ram.nes", "2ACKiuKHeQth9xxXEZtgRQUIi6w="] => "DTzrtpk/qotzzyeaPduvd/9bAg4=", 95 | ["blargg_ppu_tests_2005.09.15b/vbl_clear_time.nes", "2ACKiuKHeQth9xxXEZtgRQUIi6w="] => "K31u/aYrfTBatl+/owMIKx1qnb8=", 96 | ["blargg_ppu_tests_2005.09.15b/vram_access.nes", "2ACKiuKHeQth9xxXEZtgRQUIi6w="] => "DTzrtpk/qotzzyeaPduvd/9bAg4=", 97 | ["branch_timing_tests/1.Branch_Basics.nes", "NTpzRpbjMHVYziSDAZpwThpaDDg="] => "qhSohh6jNIOM3G7cIZr8+hbfcf4=", 98 | ["branch_timing_tests/2.Backward_Branch.nes", "BGjGkBOMnGfR2X4B2d3H/VSsPxw="] => "q0+WmzMpTdMDE62EJkGbCGDgSU8=", 99 | ["branch_timing_tests/3.Forward_Branch.nes", "S2UdyUN17QLEAbTPnM/sTGinkxo="] => "q0+WmzMpTdMDE62EJkGbCGDgSU8=", 100 | ["cpu_dummy_reads/cpu_dummy_reads.nes", "IZ7If73DZSDpOamXOmHx+MzmPBI="] => "7Vd90hahlt+FgOGTS7Za2T0ZnWk=", 101 | ["cpu_interrupts_v2/rom_singles/1-cli_latency.nes", "SpC0wIweffQZSre327sLMWsRfP4="] => "FIh2IIunZkL2rtPFLN4/UgHa3ck=", 102 | ["cpu_interrupts_v2/rom_singles/2-nmi_and_brk.nes", "G51vjIhxdNPMxGRkDStGjECiZdo="] => "pbDTvwK60s4R9Pi3je2L8o9iepg=", 103 | ["cpu_interrupts_v2/rom_singles/3-nmi_and_irq.nes", "nhdRKkcnEqojeRlTCr+F1kMz9IU="] => "8jAMSwsYvEKmkAJ6uNS8suMW89U=", 104 | ["cpu_reset/ram_after_reset.nes", "FiAsKo3Df69PZWd5r9lcCTxzKvM="] => "Fsj/D9Vt5HSiigf2ryGzPRcNOqc=", 105 | ["cpu_reset/registers.nes", "FiAsKo3Df69PZWd5r9lcCTxzKvM="] => "Fsj/D9Vt5HSiigf2ryGzPRcNOqc=", 106 | ["cpu_timing_test6/cpu_timing_test.nes", "fpbbQbbXCLSJiqSqKtGpjfhQ/Gc="] => "NmPFtldwhbtKpoJONIgYslwkTFc=", 107 | ["cpu_timing_test6/cpu_timing_test.nes", "pxjbcfJBNDWLLRn+1n1PARRTKAo="] => "/2vgArRQcGp7W4VxnHGkheup49s=", 108 | ["cpu_timing_test6/cpu_timing_test.nes", "qiCw5Tc02sYX/zr58+sSEm2thAY="] => "FGYhdM0eDiF1jEwMNKaOdJPI2fY=", 109 | ["dmc_dma_during_read4/dma_2007_write.nes", "UvqdCGEKiDqwDsHUpSsqN1BvI9Y="] => "nCyvymYwIGyo1b2JlsGTlBmBrEk=", 110 | ["dmc_dma_during_read4/double_2007_read.nes", "n8KPQ9tB6W6iemDYSyinaCXRIZI="] => "SCT3Ie8zfgGzgsX54g18a+dz20w=", 111 | ["dmc_dma_during_read4/read_write_2007.nes", "ogLiZLQg2KSbdltpnma896mtmiI="] => "hcbQiENATEK9SgtnqA0UAN4wlzI=", 112 | ["instr_misc/instr_misc.nes", "iZ2XYkUeZjv5ePYE9Md5lU8+H28="] => "YqiUhvws22w9aBXWvrUxHSxW6UU=", 113 | ["instr_misc/rom_singles/01-abs_x_wrap.nes", "WCx7tS1Mwo8NqngfulG9adk1kiM="] => "CykvKb9WfOp6Kwd32RKxBiV1Bdk=", 114 | ["instr_misc/rom_singles/02-branch_wrap.nes", "jlVAxP0SaI05NPtuUeT7Ob9iero="] => "CykvKb9WfOp6Kwd32RKxBiV1Bdk=", 115 | ["instr_misc/rom_singles/03-dummy_reads.nes", "tyTlCPdKk4iSaZJ3xdOFhBnVHuk="] => "pyl2P2yitKVZoe3fYbktKPt47cQ=", 116 | ["instr_misc/rom_singles/04-dummy_reads_apu.nes", "oORp9qLG3OmJzJHQIEjAp7XTlWE="] => "Fsj/D9Vt5HSiigf2ryGzPRcNOqc=", 117 | ["instr_test-v3/all_instrs.nes", "RBzdRMiDUkizcDzxfBgd+ahh1NM="] => "NZctmuvl68P0a/cOM43C9qOqfQE=", 118 | ["instr_test-v3/official_only.nes", "RBzdRMiDUkizcDzxfBgd+ahh1NM="] => "UH3ii97LbYXghoVHAv6AAmCmdWc=", 119 | ["instr_test-v3/rom_singles/01-implied.nes", "n7U5RnFgcdb7kFV1dZfksAqUBMs="] => "rfg0NB23WKlPhxvIP/E9BH2h7QY=", 120 | ["instr_test-v3/rom_singles/02-immediate.nes", "OYTH2t40zTRfpTnF1GKsxZ8vna8="] => "Mw3e2G3dkxOf9ttR/mmP8c9vMsc=", 121 | ["instr_test-v3/rom_singles/03-zero_page.nes", "IWJ0/os7GyhIQ8/7297rlGQmJpU="] => "JBuzZ1Yw2VddN4jXY8NbvBhlFfI=", 122 | ["instr_test-v3/rom_singles/04-zp_xy.nes", "sUn1ZLzjfc0byz6/iacouftCNaU="] => "YUxhdvXzyPzfgSfFjEQgyan4VwM=", 123 | ["instr_test-v3/rom_singles/05-absolute.nes", "y/bns/H8tdQCdiqYWMn0qzAr+00="] => "MJiVpNF2L0o9wdk6J2yO1hgyYzQ=", 124 | ["instr_test-v3/rom_singles/06-abs_xy.nes", "jS2Zgrjd3BU3Jj8qobdUWF0nxPk="] => "1V5zrgtgDMKm89xFRT7dyq9epjU=", 125 | ["instr_test-v3/rom_singles/07-ind_x.nes", "LdpOb9FUY/7uVET7saATEPXPTD0="] => "UJhkGe6BLE1P0NHtxNUNs7g3YS8=", 126 | ["instr_test-v3/rom_singles/08-ind_y.nes", "M87UDz5ijJzD1v5ioFB7dJqUXSo="] => "3nC87AdNAX3k5VXlPYHGWaGGwKw=", 127 | ["instr_test-v3/rom_singles/09-branches.nes", "WJVcKaRUZPErFU0/UISvG+x8Czw="] => "CykT9oq8cTN5imKiFWtgUrvHOKI=", 128 | ["instr_test-v3/rom_singles/10-stack.nes", "mDhsrKJkaoGI162u/ZDMjgeEZn4="] => "XFlUxQtLu0yxl/Pg7Y1mSnED3jg=", 129 | ["instr_test-v3/rom_singles/11-jmp_jsr.nes", "pn0CDLxK0Btl8ogs7cZs5s9mFig="] => "P4hl+vU2KfULEt3qKMnfc2V6Fn8=", 130 | ["instr_test-v3/rom_singles/12-rts.nes", "Q+FItBqJ35fSJUxezY7rDohGpj8="] => "sGhlDOeuO0eIe4fDJvTWY/llxhQ=", 131 | ["instr_test-v3/rom_singles/13-rti.nes", "mC53jqJUSVgt6Mab5p9vTFGF4pA="] => "6h6dFyda19rd7QrK5Cd2azeMDSA=", 132 | ["instr_test-v3/rom_singles/14-brk.nes", "SRIwi0+9JMhuZnb1SgkMfolFpSQ="] => "2xJL8U8lbirqcKffGd9WKKsxhsE=", 133 | ["instr_test-v3/rom_singles/15-special.nes", "oNLQxerG1cRgxFHLi3pWOmeHVDY="] => "WtysSS9Gt2b0KdF/G6BGWlN2DJ8=", 134 | ["instr_timing/instr_timing.nes", "J7ka+aDZntB3l83JlCXW9nTY/uY="] => "296EYJ+Q7AgBBh+oRqKfIRRFHTY=", 135 | ["instr_timing/rom_singles/1-instr_timing.nes", "ZCRfNt3EX1IneK9Ai/OiCbUwNzE="] => "A/Novd1ECXRjtevLiEGmhvQh0fc=", 136 | ["instr_timing/rom_singles/2-branch_timing.nes", "086PXJoyijU44W2y4tTDtkIGR2M="] => "ZZ3NSyG+IBmWledXwGPIXGPxlYs=", 137 | ["mmc3_irq_tests/1.Clocking.nes", "ZqkTHgTTAPpDRn9sqNad2yz5pYs="] => "nKoU9CIS+Mg1TNQVXvOiyGunpbs=", 138 | ["mmc3_irq_tests/2.Details.nes", "R026+0tGfi7uc9HyUeDCFq0sxJw="] => "JqrN9phWyedireHOKmjgr7ojJSs=", 139 | ["mmc3_irq_tests/3.A12_clocking.nes", "kQuwXXwPR/0Lwzwy6McyfEFiXDs="] => "nKoU9CIS+Mg1TNQVXvOiyGunpbs=", 140 | ["mmc3_irq_tests/4.Scanline_timing.nes", "HEO9IvZ5q+kZgHEfpldi1kMwrzA="] => "CeFd73VY8If4/VYeeX6GpT8nwgs=", 141 | ["mmc3_irq_tests/5.MMC3_rev_A.nes", "kZ+G1y5kY+7Yirs8wbD/JHQzUHs="] => "aunJndZGIh0HO7msCVirdFPkrDY=", 142 | ["mmc3_test/1-clocking.nes", "/6lQUCFnZUjfw6pW46LqKU4n6Sk="] => "2Lt0m+OFqn7dtKKA8y0cs8BkxNs=", 143 | ["mmc3_test/2-details.nes", "e6ZUPFCkoRfTNNKJsMOIv0C8pjw="] => "RsyAZ9W81udDs0jsTBOj9MKzUtQ=", 144 | ["mmc3_test/3-A12_clocking.nes", "3Srp4z0tNrT8KeU0XszHGGGXwP0="] => "2Lt0m+OFqn7dtKKA8y0cs8BkxNs=", 145 | ["mmc3_test/4-scanline_timing.nes", "wvBhqyDa7lGGy5Nyx6kMAAV2wQA="] => "4/TzDoKRKjmAnLdc4JFLlEwnJR4=", 146 | ["mmc3_test/5-MMC3.nes", "e2HtOAagMzn8vT2R47TuHtEoEGw="] => "PvELODjaLCoODEFOYHVXSgrsd9U=", 147 | ["mmc3_test/6-MMC6.nes", "1D7g0UPazJz8zECHs09dVaFrrEo="] => "pBJxYjxhgZhyzJ13ZCCk341yAQg=", 148 | ["nmi_sync/demo_ntsc.nes", "VPaA+wEVi+G1LeopdAmHRiATX1M="] => "pyl2P2yitKVZoe3fYbktKPt47cQ=", 149 | ["oam_read/oam_read.nes", "5yTFeVWQR69gVIx9N/0dNjK6bO4="] => "CykvKb9WfOp6Kwd32RKxBiV1Bdk=", 150 | ["other/PCM.demo.wgraphics.nes", "pHRC5undB25lm7rgcB7K44YpZkE="] => "207rRnocGSQWzc/zAtjc06mVAgo=", 151 | ["other/RasterChromaLuma.NES", "qvAWjQxmhejvqAhlydizmjekinc="] => "CykvKb9WfOp6Kwd32RKxBiV1Bdk=", 152 | ["other/RasterTest3.NES", "ZQDyp7EioQrVBlgUAjoxtY8NbLk="] => "CykvKb9WfOp6Kwd32RKxBiV1Bdk=", 153 | ["other/RasterTest3a.NES", "ExxlU4SEW1lZZTqvHJsxS95TToU="] => "CykvKb9WfOp6Kwd32RKxBiV1Bdk=", 154 | ["other/RasterTest3b.NES", "GQLGeg3+Qk4fv7JYweCNHvaA4Tk="] => "CykvKb9WfOp6Kwd32RKxBiV1Bdk=", 155 | ["other/RasterTest3c.NES", "KjlFw7WJNtCr13OasylAmuCY2aw="] => "CykvKb9WfOp6Kwd32RKxBiV1Bdk=", 156 | ["other/RasterTest3d.NES", "N2QzIE0OX4Bbhpx/NLPTpinu6Po="] => "CykvKb9WfOp6Kwd32RKxBiV1Bdk=", 157 | ["other/RasterTest3e.NES", "jJDtkpyMOz2NTtgbhhFi7KXZWpw="] => "CykvKb9WfOp6Kwd32RKxBiV1Bdk=", 158 | ["other/S0.NES", "x7tDPDXKlymWFCPRowQlOdQjJu4="] => "CykvKb9WfOp6Kwd32RKxBiV1Bdk=", 159 | ["other/nestest.nes", "9TB6z7tvI3VzIlngozSjdBQ6Ils="] => "iW/M8yCUxpt9+V0Lps4VOErUHZw=", 160 | ["ppu_vbl_nmi/ppu_vbl_nmi.nes", "6X5+GM6YQfB4enaqJlBrDa5Qtzo="] => "SgGSPn0ydicTU/ZneKToa+33OoU=", 161 | ["ppu_vbl_nmi/rom_singles/01-vbl_basics.nes", "CpMy2y52QJB1+Ut8CKgz9A7I344="] => "yOc3AWDQhdM5C0BId+8rg9LXRw4=", 162 | ["ppu_vbl_nmi/rom_singles/02-vbl_set_time.nes", "x5lMpbxxlMZKNkAZ3hr++SEy0Yw="] => "fpEcKZ2alK6yFVrnzlehFdeFBAs=", 163 | ["ppu_vbl_nmi/rom_singles/03-vbl_clear_time.nes", "QAVr0aXlcZpXVBtniaxXdRbazno="] => "OUNb+EDKT302cDD6ULrVfHi2Isk=", 164 | ["ppu_vbl_nmi/rom_singles/04-nmi_control.nes", "KLWQ7fq5zVi5d0PfwYWBLYCi7HY="] => "31hAYx286VKyU8ify6Vza4/Dj8c=", 165 | ["ppu_vbl_nmi/rom_singles/05-nmi_timing.nes", "p477oq82Zqm8ofQsXheCf+TCRTw="] => "cluFwHYcsnP20wDvVmkHJ+jLizM=", 166 | ["ppu_vbl_nmi/rom_singles/06-suppression.nes", "39xUI45+3b2+HH7LMGCcUNt4vKY="] => "SJl5Fe1NgjgqhuDN004PlPjm+RU=", 167 | ["ppu_vbl_nmi/rom_singles/07-nmi_on_timing.nes", "1g/TnrYgE7kiS0aaw2EdeQxl8D4="] => "d0ybf3ujcRqHWdm3e2eLHS8WmNc=", 168 | ["ppu_vbl_nmi/rom_singles/08-nmi_off_timing.nes", "29z8PGl7oPWYOP1/5cmj0/esdOo="] => "kKjjI+UICoSIA0ebd1aqbT5Ct3o=", 169 | ["ppu_vbl_nmi/rom_singles/09-even_odd_frames.nes", "l9ASihPBcYc0jKAp4LMM1gfEYP0="] => "z9hmupPjf/YpjnKIlNVb9rsbYrM=", 170 | ["ppu_vbl_nmi/rom_singles/10-even_odd_timing.nes", "UpPRP5OVU51XTAMS7RUE8iak/BI="] => "S8UzpC78XzZYcLAL/hJbFvurfYc=", 171 | ["read_joy3/test_buttons.nes", "zr4miqOZKgHF0LMqQqYckcxINbY="] => "YwXAIBxfvdFvnN2qiHwPtofNXH0=", 172 | ["read_joy3/thorough_test.nes", "z7/v0RtA9ptZx2NzMmfVMhKIL14="] => "FszUM0cBFYkl9GLYJ498yEzk8HI=", 173 | ["sprite_hit_tests_2005.10.05/01.basics.nes", "g/VxI/pEE1YgYC6i1WYWhEu39N4="] => "PplyMf9BlIO8npD1pR278A/wLVA=", 174 | ["sprite_hit_tests_2005.10.05/02.alignment.nes", "Sg/MGfJNAOW5g2iCM2QGzRONbhM="] => "boBCdv8q+mU1ehMctIDdb8B+a00=", 175 | ["sprite_hit_tests_2005.10.05/03.corners.nes", "V3ICSP+38/Z6SqOeQiYhKLQOW5w="] => "5EMUi7MJ9mMgdeVAEk/4dy50+xI=", 176 | ["sprite_hit_tests_2005.10.05/04.flip.nes", "ejt5YTdLSEzx4oETy306J0tZoko="] => "nKoU9CIS+Mg1TNQVXvOiyGunpbs=", 177 | ["sprite_hit_tests_2005.10.05/05.left_clip.nes", "Cwde8FZMs6z3n1NDQHLsCVgsPQs="] => "Yuv+BICPLOLHepBiuEFWPjw7kR4=", 178 | ["sprite_hit_tests_2005.10.05/06.right_edge.nes", "Usj4WtxKj+6yjiAtjvt79cBBtOE="] => "JqrN9phWyedireHOKmjgr7ojJSs=", 179 | ["sprite_hit_tests_2005.10.05/07.screen_bottom.nes", "Wqt8ZHLfPp4BYy5MCsC2JCngqqw="] => "p3IjEio2yNsldPzm5KLEjyYeYrA=", 180 | ["sprite_hit_tests_2005.10.05/08.double_height.nes", "DfMiV6YRYgPxD+1B3T3FTuv+YJM="] => "5EMUi7MJ9mMgdeVAEk/4dy50+xI=", 181 | ["sprite_hit_tests_2005.10.05/09.timing_basics.nes", "+dRfx/nvSLg4Gls5cGwKB4WQD5E="] => "qfbgrs9dX+24ukO+KJuz+l61SBM=", 182 | ["sprite_hit_tests_2005.10.05/10.timing_order.nes", "rqcJD3McCNwA8LUu6SH2pAoMvUs="] => "Pdk2CCE92RGAYG1BGk7wYF0eEHI=", 183 | ["sprite_hit_tests_2005.10.05/11.edge_timing.nes", "I/QgailO8jvJADJbgXd2Wiztnhg="] => "EK6ip3zJLgMDY5OogCdKu+38gnE=", 184 | ["sprite_overflow_tests/1.Basics.nes", "j9zIKsi6wv884n3xjT1Y3aopymU="] => "yHFbGhrR/TQu4Qx5tIoPYs3tmj4=", 185 | ["sprite_overflow_tests/2.Details.nes", "Z1TvJ6ADX3xKIhAfPTK28VEnGAE="] => "aYD4/RFWcpkfZfyYZAQytxvCisI=", 186 | ["sprite_overflow_tests/3.Timing.nes", "YGCIdXFdv1QPGu4dX4SVOVDv18M="] => "gw++hMBS9AA/0zYpMt8kx/S0iHg=", 187 | ["sprite_overflow_tests/4.Obscure.nes", "G7QTo/aa6XTtLYiJuYep+JBoIyQ="] => "GkO5GKFfUQUalIsX4cHIVYl8Ao0=", 188 | ["sprite_overflow_tests/5.Emulator.nes", "FIMmXK96ioafYAgjHFtUDpJBUk0="] => "6BRNT7ff6Cd+fthGiS2Ke+4DlM8=", 189 | ["stomper/smwstomp.nes", "kCn0N3p5wTqvDiM8jKaLNzE9qpc="] => "CykvKb9WfOp6Kwd32RKxBiV1Bdk=", 190 | ["vbl_nmi_timing/1.frame_basics.nes", "92MKeu+BNV2FPH3kv1/K9bMxjrk="] => "1yAaANmuSLHLmpkAUIfSpEVQS/8=", 191 | ["vbl_nmi_timing/2.vbl_timing.nes", "W7dVlXd44bcC1IiV4leiH74T7mk="] => "4YfurCjuak9J8qEOu+uOq03wjXc=", 192 | ["vbl_nmi_timing/3.even_odd_frames.nes", "k+smsz5p87yWCYdp1OKa1YaXRQk="] => "ueJGL2HWMo+E6COlpAOFeN/JXVc=", 193 | ["vbl_nmi_timing/4.vbl_clear_timing.nes", "/ZLeXZYpV/qwGX7FfKRAjxn0otE="] => "xSs/gOC/DHG9w3umfuyyDLYdSMM=", 194 | ["vbl_nmi_timing/5.nmi_suppression.nes", "dj7JK/m85c5RceEBNDgxgRuRqw8="] => "5KRO6QzHUuepoi6gqK8IN0nts08=", 195 | ["vbl_nmi_timing/6.nmi_disable.nes", "tIJKYXx4bCWegJzob7wDNqXfYk0="] => "tF7TZ4GW++l6UxKySjL7Qh8XnbE=", 196 | ["vbl_nmi_timing/7.nmi_timing.nes", "7qr77ue+0LN1Rr3g51kSfjNTCj8="] => "tF7TZ4GW++l6UxKySjL7Qh8XnbE=", 197 | } 198 | # rubocop:enable Layout/LineLength 199 | 200 | # parse nes-test-roms/test_roms.xml 201 | Test = Struct.new(:runframes, :filename, :filepath, :tvsha1, :input_log) 202 | TESTS = [] 203 | File.open(File.join(TEST_DIR, "test_roms.xml")) {|io| REXML::Document.new(io) }.root.elements.each do |elem| 204 | # pal is not supported 205 | next if elem.attributes["system"] == "pal" 206 | filename = elem.attributes["filename"].tr("\\", "/") 207 | runframes = elem.attributes["runframes"].to_i 208 | 209 | runframes = 4000 if filename == "instr_timing/instr_timing.nes" 210 | filename = "stress/NEStress.NES" if filename == "stress/NEStress.nes" 211 | 212 | filepath = File.join(TEST_DIR, filename) 213 | tvsha1 = elem.elements["tvsha1"].text 214 | input_log = [] 215 | elem.elements["recordedinput"].text.unpack1("m").scan(/.{5}/m) do |s| 216 | cycle, data = s.unpack("VC") 217 | frame = (cycle.to_f / 29780.5).round 218 | input_log[frame] ||= 0 219 | input_log[frame] |= data 220 | end 221 | TESTS << Test[runframes, filename, filepath, tvsha1, input_log] 222 | end 223 | 224 | # ad-hoc patch 225 | TESTS.each do |test| 226 | case test.filename 227 | when "cpu_interrupts_v2/rom_singles/1-cli_latency.nes" 228 | test.tvsha1 = "SpC0wIweffQZSre327sLMWsRfP4=" 229 | when "mmc3_test/4-scanline_timing.nes" 230 | test.runframes = 360 231 | test.tvsha1 = "wvBhqyDa7lGGy5Nyx6kMAAV2wQA=" 232 | when "mmc3_test/5-MMC3.nes" 233 | test.tvsha1 = "e2HtOAagMzn8vT2R47TuHtEoEGw=" 234 | when "mmc3_irq_tests/6.MMC3_rev_B.nes" 235 | test.tvsha1 = "1D7g0UPazJz8zECHs09dVaFrrEo=" 236 | end 237 | end 238 | 239 | if ARGV.empty? 240 | require "open3" 241 | TESTS.reject! {|test| EXCLUDES.include?(test.filename) } 242 | threads = [] 243 | queue = Queue.new 244 | 4.times do 245 | threads << Thread.new do 246 | while true 247 | test = TESTS.shift 248 | break unless test 249 | queue << Open3.capture3("ruby", __FILE__, test.filepath) 250 | end 251 | queue << nil 252 | end 253 | end 254 | num_pass = num_fail = 0 255 | while threads.any? {|th| th.alive? } 256 | out, _, status = queue.shift 257 | next unless out 258 | puts out 259 | if status.success? 260 | num_pass += 1 261 | else 262 | num_fail += 1 263 | end 264 | end 265 | puts "pass: #{ num_pass }, fail: #{ num_fail }" 266 | else 267 | if ARGV[0] != "cov" 268 | argv = ARGV.map {|file| File.expand_path(file) } 269 | TESTS.select! do |test| 270 | argv.include?(test.filepath) 271 | end 272 | else 273 | require "simplecov" 274 | SimpleCov.start 275 | TESTS.reject! {|test| EXCLUDES.include?(test.filename) } 276 | end 277 | 278 | require_relative "../lib/optcarrot" 279 | TESTS.each do |test| 280 | begin 281 | nes = Optcarrot::NES.new( 282 | romfile: test.filepath, 283 | video: :png, 284 | audio: :wav, 285 | input: :log, 286 | frames: test.runframes, 287 | key_log: test.input_log, 288 | sprite_limit: true, 289 | opt_ppu: [:all], 290 | opt_cpu: [:all], 291 | ) 292 | nes.reset 293 | sha1s = [] 294 | test.runframes.times do 295 | nes.step 296 | v = nes.instance_variable_get(:@ppu).output_pixels[0, 256 * 240].flat_map do |r, g, b| 297 | [r, g, b, 255] 298 | end 299 | sha1 = Digest::SHA1.base64digest(v.pack("C*")) 300 | sha1s << sha1 301 | end 302 | raise "video: #{ test.tvsha1 } #{ sha1s.last }" unless sha1s.include?(test.tvsha1) 303 | 304 | sha1 = Digest::SHA1.base64digest(nes.instance_variable_get(:@audio).instance_variable_get(:@buff).pack("v*")) 305 | 306 | unless SOUND_SHA1[[test.filename, test.tvsha1]] == sha1 307 | raise "sound: #{ SOUND_SHA1[[test.filename, test.tvsha1]] } #{ sha1 }" 308 | end 309 | 310 | puts "ok: " + test.filename 311 | $stdout.flush 312 | rescue Interrupt 313 | raise 314 | rescue 315 | puts "NG: " + test.filename 316 | # rubocop:disable Style/SpecialGlobalVars 317 | p $! 318 | p(*$!.backtrace) 319 | # rubocop:enable Style/SpecialGlobalVars 320 | exit 1 321 | end 322 | end 323 | end 324 | -------------------------------------------------------------------------------- /tools/shim.rb: -------------------------------------------------------------------------------- 1 | # This is a shim for Ruby implementations other than MRI 2. 2 | # 3 | # Fortunately, most of these methods are not used in hot-spot (except 4 | # Array#rotate!), so I don't think that this shim will degrade the performance. 5 | # However, some implementations may stop optimization when a built-in classes 6 | # are modified by monkey-patching. In this case, the speed will be reduced. 7 | 8 | # I want to make this shim so simple that you don't need doc... 9 | # rubocop:disable Style/Documentation 10 | 11 | RUBY_ENGINE = "ruby" if RUBY_VERSION == "1.8.7" && !Module.const_defined?(:RUBY_ENGINE) 12 | if RUBY_ENGINE == "opal" 13 | require "opal-parser" # for eval 14 | require "nodejs" 15 | end 16 | 17 | unless [].respond_to?(:rotate!) 18 | # Array#rotate! is used in hotspot; this shim will reduce the performance terribly. 19 | # This shim is for MRI 1.8.7. 1.8.7 has a handicap. 20 | $stderr.puts "[shim] Array#rotate!" 21 | class Array 22 | def rotate!(n) 23 | if n > 0 24 | concat(shift(n)) 25 | elsif n < 0 26 | unshift(*pop(-n)) 27 | end 28 | self 29 | end 30 | end 31 | end 32 | 33 | unless [].respond_to?(:slice!) 34 | $stderr.puts "[shim] Array#slice!" 35 | class Array 36 | def slice!(_zero_assumed, len) 37 | a = [] 38 | len.times { a << shift } 39 | a 40 | end 41 | end 42 | end 43 | 44 | unless [].respond_to?(:flat_map) 45 | $stderr.puts "[shim] Array#flat_map" 46 | class Array 47 | def flat_map(&blk) 48 | map(&blk).flatten(1) 49 | end 50 | end 51 | end 52 | 53 | unless [].respond_to?(:transpose) 54 | $stderr.puts "[shim] Array#transpose" 55 | class Array 56 | def transpose 57 | ret = self[0].map { [] } 58 | self[0].size.times do |i| 59 | size.times do |j| 60 | ret[i] << self[j][i] 61 | end 62 | end 63 | ret 64 | end 65 | end 66 | end 67 | 68 | if ![].respond_to?(:freeze) || RUBY_ENGINE == "opal" 69 | $stderr.puts "[shim] Array#freeze" 70 | class Array 71 | def freeze 72 | self 73 | end 74 | end 75 | end 76 | 77 | if RUBY_ENGINE == "opal" 78 | require "corelib/array/pack" 79 | end 80 | 81 | unless [].respond_to?(:pack) && [33, 33].pack("C*") == "!!" 82 | $stderr.puts "[shim] Array#pack" 83 | class Array 84 | alias pack_orig pack if [].respond_to?(:pack) 85 | def pack(fmt) 86 | if fmt == "C*" 87 | map {|n| n.chr }.join 88 | else 89 | pack_orig(fmt) 90 | end 91 | end 92 | end 93 | end 94 | 95 | if {}.respond_to?(:compare_by_identity) 96 | # https://github.com/jruby/jruby/issues/3650 97 | h = {}.compare_by_identity 98 | a = [0] 99 | h[a] = 42 100 | a[0] = 1 101 | need_custom_identity_hash = !h[a] 102 | else 103 | need_custom_identity_hash = true 104 | end 105 | if need_custom_identity_hash 106 | $stderr.puts "[shim] Hash#compare_by_identity" 107 | # rubocop:disable Lint/HashCompareByIdentity 108 | class IdentityHash 109 | def initialize 110 | @h = {} 111 | end 112 | 113 | def [](key) 114 | @h[key.object_id] 115 | end 116 | 117 | def []=(key, val) 118 | @h[key.object_id] = val 119 | end 120 | end 121 | # rubocop:enable Lint/HashCompareByIdentity 122 | 123 | class Hash 124 | def compare_by_identity 125 | IdentityHash.new 126 | end 127 | end 128 | end 129 | 130 | unless "".respond_to?(:b) 131 | $stderr.puts "[shim] String#b" 132 | class String 133 | def b 134 | self 135 | end 136 | end 137 | end 138 | 139 | unless "".respond_to?(:sum) 140 | $stderr.puts "[shim] String#sum" 141 | class String 142 | def sum(bits = 16) 143 | s = 0 144 | each_byte {|c| s += c } 145 | return 0 if s == 0 146 | s & ((1 << bits) - 1) 147 | end 148 | end 149 | end 150 | 151 | unless "".respond_to?(:bytes) && "".bytes == [] 152 | if "".respond_to?(:unpack) 153 | $stderr.puts "[shim] String#bytes (by using unpack)" 154 | class String 155 | remove_method(:bytes) if "".respond_to?(:bytes) 156 | def bytes 157 | unpack("C*") 158 | end 159 | end 160 | else 161 | class String 162 | $stderr.puts "[shim] String#bytes (by aliasing)" 163 | alias bytes_orig bytes 164 | def bytes 165 | bytes_orig.to_a 166 | end 167 | end 168 | end 169 | end 170 | 171 | if RUBY_ENGINE == "opal" 172 | $stderr.puts "[shim] String#bytes (force_encoding)" 173 | class String 174 | alias bytes_orig bytes 175 | def bytes 176 | a = [] 177 | bytes_orig.each_slice(2) {|b0, _b1| a << b0 } 178 | a 179 | end 180 | end 181 | end 182 | 183 | if !"".respond_to?(:tr) || Module.const_defined?(:Topaz) 184 | $stderr.puts "[shim] String#tr" 185 | class String 186 | alias tr gsub 187 | end 188 | end 189 | 190 | if !"".respond_to?(:%) || Module.const_defined?(:Topaz) 191 | # Topaz aborts when evaluating String#%... 192 | $stderr.puts "[shim] String#%" 193 | class String 194 | def %(*_args) 195 | "" 196 | end 197 | end 198 | end 199 | 200 | unless "".respond_to?(:unpack) 201 | $stderr.puts "[shim] String#unpack" 202 | class String 203 | def unpack(fmt) 204 | if fmt == "C*" 205 | return each_byte.to_a.map {|ch| ch.ord } 206 | else 207 | raise 208 | end 209 | end 210 | end 211 | end 212 | 213 | unless 0.respond_to?(:[]) && -1[0] == 1 214 | $stderr.puts "[shim] Fixnum#[]" 215 | # rubocop:disable Lint/UnifiedInteger 216 | class Fixnum 217 | # rubocop:enable Lint/UnifiedInteger 218 | def [](i) 219 | (self >> i) & 1 220 | end 221 | end 222 | end 223 | 224 | unless 0.respond_to?(:even?) 225 | $stderr.puts "[shim] Fixnum#even?" 226 | # rubocop:disable Lint/UnifiedInteger 227 | class Fixnum 228 | # rubocop:enable Lint/UnifiedInteger 229 | def even? 230 | # rubocop:disable Style/EvenOdd 231 | self % 2 == 0 232 | # rubocop:enable Style/EvenOdd 233 | end 234 | end 235 | end 236 | 237 | begin 238 | 1.step(3, 2) 239 | rescue LocalJumpError 240 | $stderr.puts "[shim] Fixnum#step without block" 241 | # rubocop:disable Lint/UnifiedInteger 242 | class Fixnum 243 | # rubocop:enable Lint/UnifiedInteger 244 | alias step_org step 245 | def step(*args, &blk) 246 | if blk 247 | step_org(*args, &blk) 248 | else 249 | # rubocop:disable Lint/ToEnumArguments 250 | enum_for(:step_org, *args) 251 | # rubocop:enable Lint/ToEnumArguments 252 | end 253 | end 254 | end 255 | end 256 | 257 | unless Kernel.respond_to?(:__dir__) 258 | $stderr.puts "[shim] Kernel#__dir__" 259 | def __dir__ 260 | File.join(File.dirname(File.dirname(__FILE__)), "bin") 261 | end 262 | end 263 | 264 | unless Kernel.respond_to?(:require) 265 | $stderr.puts "[shim] Kernel#require" 266 | DIRS = %w(lib lib/optcarrot).map {|f| File.join(File.dirname(File.dirname(__FILE__)), f) } 267 | $LOAD_PATH = [] 268 | LOADED = {} 269 | def require(f) 270 | f = DIRS.map {|d| File.join(d, f + ".rb") }.find {|fn| File.exist?(fn) } 271 | return if LOADED[f] 272 | LOADED[f] = true 273 | eval(File.read(f), nil, f) 274 | end 275 | end 276 | 277 | unless Kernel.respond_to?(:require_relative) 278 | $stderr.puts "[shim] Kernel#require_relative" 279 | dir = File.join(File.dirname(File.dirname(__FILE__)), "lib") 280 | $LOAD_PATH << dir << File.join(dir, "optcarrot") 281 | unless RUBY_ENGINE == "opal" 282 | def require_relative(f) 283 | f = "optcarrot" if f == "../lib/optcarrot" 284 | require(f) 285 | end 286 | end 287 | end 288 | 289 | unless File.respond_to?(:extname) 290 | $stderr.puts "[shim] File.extname" 291 | def File.extname(f) 292 | f =~ /\..*\z/ 293 | $& 294 | end 295 | end 296 | 297 | if RUBY_ENGINE == "opal" 298 | $stderr.puts "[shim] File.binread (for opal/nodejs)" 299 | class Blob 300 | def initialize(buf) 301 | @buf = buf 302 | end 303 | 304 | # rubocop:disable Style/CommandLiteral 305 | def bytes 306 | %x{ 307 | var __buf__ = #{ @buf }; 308 | var __ary__ = []; 309 | for (var i = 0, length = __buf__.length; i < length; i++) { 310 | __ary__.push(__buf__[i]); 311 | } 312 | return __ary__; 313 | } 314 | end 315 | # rubocop:enable Style/CommandLiteral 316 | end 317 | 318 | class File 319 | def self.binread(f) 320 | Blob.new(`#{ node_require(:fs) }.readFileSync(#{ f })`) 321 | end 322 | end 323 | elsif !File.respond_to?(:binread) 324 | $stderr.puts "[shim] File.binread (by using open)" 325 | class File 326 | def self.binread(file) 327 | File.open(file, "rb") {|f| f.read } 328 | end 329 | end 330 | end 331 | 332 | unless Module.const_defined?(:Process) 333 | module Process 334 | end 335 | end 336 | unless Process.respond_to?(:clock_gettime) && Process.const_defined?(:CLOCK_MONOTONIC) 337 | if RUBY_ENGINE == "mruby" 338 | $stderr.puts "[shim] Process.clock_gettime for mruby (MRB_WITHOUT_FLOAT)" 339 | class DummyTime 340 | def initialize 341 | t = gettimeofday 342 | @usec = t[:tv_sec] * 1_000_000 + t[:tv_usec] 343 | end 344 | attr_reader :usec 345 | def -(other) 346 | MFloat.new(@usec - other.usec) 347 | end 348 | end 349 | 350 | class MFloat 351 | def initialize(val) 352 | @val = val 353 | end 354 | 355 | def /(other) 356 | MFloat.new(@val / other) 357 | end 358 | 359 | def **(other) 360 | raise if other != -1 361 | MFloat.new(1_000_000_000_000 / @val) 362 | end 363 | 364 | def to_s 365 | (@val / 1_000_000).to_s + "." + (@val % 1_000_000).to_s.rjust(6, "0") 366 | end 367 | end 368 | 369 | def Process.clock_gettime(*) 370 | DummyTime.new 371 | end 372 | else 373 | $stderr.puts "[shim] Process.clock_gettime by Time" 374 | def Process.clock_gettime(*) 375 | Time.now.to_f 376 | end 377 | end 378 | Process::CLOCK_MONOTONIC = nil unless Process.const_defined?(:CLOCK_MONOTONIC) 379 | end 380 | 381 | module M 382 | module_function 383 | 384 | def foo 385 | end 386 | end 387 | unless M.respond_to?(:foo) 388 | $stderr.puts "[shim] Module#module_function" 389 | class Module 390 | def module_function 391 | extend(self) 392 | end 393 | end 394 | end 395 | 396 | unless "".method(:b).respond_to?(:[]) 397 | $stderr.puts "[shim] Method#[]" 398 | class Method 399 | alias [] call 400 | end 401 | end 402 | 403 | if !Module.const_defined?(:Fiber) && RUBY_ENGINE != "opal" 404 | $stderr.puts "[shim] Fiber" 405 | require "thread" # rubocop:disable Lint/RedundantRequireStatement 406 | 407 | Thread.abort_on_exception = true 408 | class Fiber 409 | # rubocop:disable Style/ClassVars 410 | def initialize 411 | @@mutex1 = Mutex.new 412 | @@mutex2 = Mutex.new 413 | @@mutex1.lock 414 | @@mutex2.lock 415 | Thread.new do 416 | @@mutex1.lock 417 | yield 418 | @@mutex2.unlock 419 | end 420 | end 421 | 422 | def resume 423 | @@mutex1.unlock 424 | @@mutex2.lock 425 | @@value 426 | end 427 | 428 | def self.yield(v = nil) 429 | @@mutex2.unlock 430 | @@value = v 431 | @@mutex1.lock 432 | end 433 | # rubocop:enable Style/ClassVars 434 | end 435 | end 436 | 437 | # directly executes bin/optcarrt since mruby does not support -r option 438 | if RUBY_ENGINE == "mruby" 439 | eval(File.read(File.join(File.dirname(File.dirname(__FILE__)), "bin/optcarrot"))) 440 | end 441 | 442 | # rubocop:enable Style/Documentation 443 | -------------------------------------------------------------------------------- /tools/statistic-test.rb: -------------------------------------------------------------------------------- 1 | require "statsample" 2 | 3 | rom = "examples/Lan_Master.nes" 4 | cmd_current = "ruby -Ilib bin/optcarrot --benchmark " + rom 5 | cmd_original = "ruby -Ilib ../optcarrot.master/bin/optcarrot --benchmark " + rom 6 | 7 | def measure(cmd) 8 | `#{ cmd }`[/fps: (\d+\.\d+)/, 1].to_f 9 | end 10 | 11 | current, original = [], [] 12 | 13 | puts "current\toriginal (in fps)" 14 | (ARGV[0] || 30).to_i.times do |i| 15 | if i.even? 16 | current << measure(cmd_current) 17 | original << measure(cmd_original) 18 | else 19 | original << measure(cmd_original) 20 | current << measure(cmd_current) 21 | end 22 | puts "%2.3f\t%2.3f" % [current.last, original.last] 23 | end 24 | 25 | t = Statsample::Test.t_two_samples_independent(current.to_vector, original.to_vector) 26 | p_val = t.probability_not_equal_variance 27 | 28 | puts 29 | puts t.summary 30 | if p_val < 0.05 31 | puts "p-value is %.3f < 0.05; there IS a significant difference" % p_val 32 | puts "Congratulations, your optimization is confirmed!" if current.mean > original.mean 33 | else 34 | puts "p-value is %.3f >= 0.05; There is NO significant differences" % p_val 35 | end 36 | --------------------------------------------------------------------------------