#!/usr/bin/env ruby require 'fileutils' require 'open3' require 'set' require 'tmpdir' require 'yaml' require 'stringio' KEYWORDS = <' STDERR.puts 'Options:' STDERR.puts ' --max-frames ' STDERR.puts ' --error-log-size (default: 20)' STDERR.puts ' --no-animation' exit(1) end @have_dot = `dot -V 2>&1`.strip[0, 3] == 'dot' @files_dir = 'report-files' FileUtils.rm_rf(@files_dir) FileUtils.mkpath(@files_dir) @max_frames = nil @record_frames = true @cycles_per_function = {} @execution_log = [] @execution_log_size = 20 @code_for_pc = {} @source_for_file = {} @max_source_width_for_file = {} @pc_for_file_and_line = {} args = ARGV.dup while args.size > 1 item = args.shift if item == '--max-frames' @max_frames = args.shift.to_i elsif item == '--error-log-size' @execution_log_size = args.shift.to_i elsif item == '--no-animation' @record_frames = false else STDERR.puts "Invalid argument: #{item}" exit(1) end end config_path = args.shift @config = YAML.load(File.read(config_path)) @source_files = [] @config['load'].each_pair do |address, path| fixed_path = path.dup unless fixed_path[0] == '/' fixed_path = File.absolute_path(File.join(File.dirname(config_path), fixed_path)) end @source_files << { :path => fixed_path, :address => address } end @highlight_color = '#fce98d' if @config['highlight'] @highlight_color = @config['highlight'] end @histogram_color = '#12959f' @keywords = Set.new(KEYWORDS.split(/\s/).map { |x| x.strip }.reject { |x| x.empty? }) @global_variables = {} @watches = {} @watches_for_index = [] @label_for_pc = {} @pc_for_label = {} # init disk image to zeroes @disk_image = [0] * 0x10000 # load empty disk image load_image('empty', 0) @source_files.each do |source_file| @source_path = File.absolute_path(source_file[:path]) @source_line = 0 unless File.exist?(@source_path) STDERR.puts 'Input file not found.' exit(1) end if @source_path[-2, 2] == '.s' Dir::mktmpdir do |temp_dir| FileUtils.cp(@source_path, temp_dir) pwd = Dir.pwd Dir.chdir(temp_dir) # compile the source merlin_output = `Merlin32 -V . #{File.basename(@source_path)}` if $?.exitstatus != 0 || File.exist?('error_output.txt') STDERR.puts merlin_output exit(1) end # collect Merlin output files @merlin_output_path = File.absolute_path(Dir['*_Output.txt'].first) @merlin_binary_path = @merlin_output_path.sub('_Output.txt', '') # parse Merlin output files # TODO: Adjust addresses!!! parse_merlin_output(@merlin_output_path) load_image(@merlin_binary_path, source_file[:address]) Dir.chdir(pwd) end else load_image(@source_path, source_file[:address]) end end end def load_image(path, address) # puts "[#{sprintf('0x%04x', address)} - #{sprintf('0x%04x', address + File.size(path) - 1)}] - loading #{File.basename(path)}" File::binread(path).unpack('C*').each.with_index { |b, i| @disk_image[i + address] = b } end def run Dir::mktmpdir do |temp_dir| if @config.include?('instant_rts') @config['instant_rts'].each do |label| @disk_image[@pc_for_label[label]] = 0x60 # insert RTS end end File.binwrite(File.join(temp_dir, 'disk_image'), @disk_image.pack('C*')) # build watch input for C program io = StringIO.new watch_index = 0 @watches.keys.sort.each do |pc| @watches[pc].each do |watch0| watch0[:components].each do |watch| if watch.include?(:register) || watch.include?(:address) which = watch.include?(:register) ? sprintf('reg,%s', watch[:register]) : sprintf('mem,0x%04x', watch[:address]) io.puts sprintf('%d,0x%04x,%d,%s,%s', watch_index, pc, watch0[:post] ? 1 : 0, watch[:type], which) end end @watches_for_index << watch0 watch_index += 1 end end watch_input = io.string @watch_values = {} @watch_called_from_subroutine = {} start_pc = @pc_for_label[@config['entry']] || @config['entry'] Signal.trap('INT') do puts 'Killing 65C02 profiler...' throw :sigint end @frame_count = 0 cycle_count = 0 last_frame_time = 0 frame_cycles = [] @total_cycles_per_function = {} @calls_per_function = {} @call_graph_counts = {} @max_cycle_count = 0 call_stack = [] last_call_stack_cycles = 0 Open3.popen2("./p65c02 #{@record_frames ? '' : '--no-screen'} --start-pc #{start_pc} #{File.join(temp_dir, 'disk_image')}") do |stdin, stdout, thread| stdin.puts watch_input.split("\n").size stdin.puts watch_input stdin.close catch :sigint do gi = nil go = nil gt = nil if @record_frames gi, go, gt = Open3.popen2("./pgif 280 192 2 > #{File.join(@files_dir, 'frames.gif')}") gi.puts '000000' gi.puts 'ffffff' end stdout.each_line do |line| # puts "> #{line}" parts = line.split(' ') if parts.first == 'error' parts.shift pc = parts.shift.to_i(16) message = parts.join(' ') @error = {:pc => pc, :message => message} elsif parts.first == 'log' parts.shift log = parts.map { |x| x.to_i(16) } @execution_log << log while @execution_log.size > @execution_log_size @execution_log.shift end elsif parts.first == 'jsr' pc = parts[1].to_i(16) cycles = parts[2].to_i @max_cycle_count = cycles @calls_per_function[pc] ||= 0 @calls_per_function[pc] += 1 calling_function = start_pc unless call_stack.empty? calling_function = call_stack.last @total_cycles_per_function[call_stack.last] ||= 0 @total_cycles_per_function[call_stack.last] += cycles - last_call_stack_cycles end @call_graph_counts[calling_function] ||= {} @call_graph_counts[calling_function][pc] ||= 0 @call_graph_counts[calling_function][pc] += 1 last_call_stack_cycles = cycles call_stack << pc elsif parts.first == 'rts' cycles = parts[1].to_i @max_cycle_count = cycles last_cycles = @total_cycles_per_function[call_stack.last] || 0 unless call_stack.empty? @total_cycles_per_function[call_stack.last] ||= 0 @total_cycles_per_function[call_stack.last] += cycles - last_call_stack_cycles end if @cycles_per_function.include?(call_stack.last) @cycles_per_function[call_stack.last] << { :call_cycles => @total_cycles_per_function[call_stack.last] - last_cycles, :at_cycles => cycles } end last_call_stack_cycles = cycles call_stack.pop elsif parts.first == 'watch' watch_index = parts[2].to_i cycles = parts[3].to_i @max_cycle_count = cycles @watch_called_from_subroutine[watch_index] ||= Set.new() @watch_called_from_subroutine[watch_index] << parts[1].to_i(16) @watch_values[watch_index] ||= [] watch_value_tuple = parts[4, parts.size - 4].map { |x| x.to_i } @watch_values[watch_index] << {:tuple => watch_value_tuple, :cycles => cycles} elsif parts.first == 'screen' @frame_count += 1 print "\rFrames: #{@frame_count}, Cycles: #{cycle_count}" this_frame_cycles = parts[1].to_i @max_cycle_count = this_frame_cycles frame_cycles << this_frame_cycles if @record_frames data = parts[2, parts.size - 2].map { |x| x.to_i } gi.puts 'l' (0...192).each do |y| (0...280).each do |x| b = (data[y * 40 + (x / 7)] >> (x % 7)) & 1 gi.print b end gi.puts end gi.puts "d #{(this_frame_cycles - last_frame_time) / 10000}" end last_frame_time = this_frame_cycles if @max_frames && @frame_count >= @max_frames break end elsif parts.first == 'cycles' cycle_count = parts[1].to_i @max_cycle_count = cycle_count print "\rFrames: #{@frame_count}, Cycles: #{cycle_count}" end end if @record_frames gi.close gt.join end end end puts @cycles_per_frame = [] (2...frame_cycles.size).each do |i| @cycles_per_frame << frame_cycles[i] - frame_cycles[i - 1] end end end def print_c(pixels, width, height, x, y, c, color) if c.ord >= 0x20 && c.ord < 0x80 font_index = (c.ord - 0x20) * 5 (0...5).each do |px| (0...7).each do |py| if ((FONT[font_index + px] >> py) & 1) == 1 pixels[(y + py) * width + (x + px)] = color end end end end end def print_c_r(pixels, width, height, x, y, c, color) if c.ord >= 0x20 && c.ord < 0x80 font_index = (c.ord - 0x20) * 5 (0...5).each do |px| (0...7).each do |py| if ((FONT[font_index + px] >> (6 - py)) & 1) == 1 pixels[(y + px) * width + (x + py)] = color end end end end end def print_s(pixels, width, height, x, y, s, color) s.each_char.with_index do |c, i| print_c(pixels, width, height, x + i * 6, y, c, color) end end def print_s_r(pixels, width, height, x, y, s, color) s.each_char.with_index do |c, i| print_c_r(pixels, width, height, x, y + i * 6, c, color) end end def write_report html_name = 'report.html' print "Writing report to file://#{File.absolute_path(html_name)} ..." File::open(html_name, 'w') do |f| report = DATA.read # write frames io = StringIO.new if @record_frames io.puts "
" end if @cycles_per_frame.size > 0 io.puts '

' io.puts "Frames recorded: #{@frame_count}
" io.puts "Average cycles/frame: #{@cycles_per_frame.inject(0) { |sum, x| sum + x } / @cycles_per_frame.size}
" io.puts '

' end report.sub!('#{screenshots}', io.string) # write watches io = StringIO.new @watches_for_index.each.with_index do |watch, index| io.puts "

" if @watch_values.include?(index) || @cycles_per_function.include?(watch[:pc]) pixels = nil width = nil height = nil histogram = {} histogram_x = {} histogram_y = {} mask = [ [0,0,1,1,1,0,0], [0,1,1,1,1,1,0], [1,1,1,1,1,1,1], [1,1,1,1,1,1,1], [1,1,1,1,1,1,1], [0,1,1,1,1,1,0], [0,0,1,1,1,0,0] ] if @watch_values.include?(index) @watch_values[index].each do |item| normalized_item = [] if item[:tuple].size == 1 normalized_item << (item[:cycles] * 255 / @max_cycle_count).to_i end item[:tuple].each.with_index do |x, i| if watch[:components][i][:type] == 's8' x += 128 elsif watch[:components][i][:type] == 'u16' x >>= 8 elsif watch[:components][i][:type] == 's16' x = (x + 32768) >> 8 end normalized_item << x end offset = normalized_item.reverse.inject(0) { |x, y| (x << 8) + y } histogram[offset] ||= 0 histogram[offset] += 1 histogram_x[normalized_item[0]] ||= 0 histogram_x[normalized_item[0]] += 1 histogram_y[normalized_item[1]] ||= 0 histogram_y[normalized_item[1]] += 1 end else max_cycle_count_for_function = @cycles_per_function[watch[:pc]].map do |x| x[:call_cycles] end.max @cycles_per_function[watch[:pc]].each do |entry| normalized_item = [] normalized_item << (entry[:at_cycles] * 255 / @max_cycle_count).to_i normalized_item << (entry[:call_cycles] * 255 / max_cycle_count_for_function).to_i offset = normalized_item.reverse.inject(0) { |x, y| (x << 8) + y } histogram[offset] ||= 0 histogram[offset] += 1 histogram_x[normalized_item[0]] ||= 0 histogram_x[normalized_item[0]] += 1 histogram_y[normalized_item[1]] ||= 0 histogram_y[normalized_item[1]] += 1 end end histogram_max = histogram.values.max histogram_x_max = histogram_x.values.max histogram_y_max = histogram_y.values.max canvas_width = 200 canvas_height = 200 histogram_height = 32 canvas_top = 10 + histogram_height canvas_left = 30 canvas_right = 10 + histogram_height canvas_bottom = 50 width = canvas_width + canvas_left + canvas_right height = canvas_height + canvas_top + canvas_bottom pixels = [0] * width * height histogram.each_pair do |key, value| x = key & 0xff; y = ((key >> 8) & 0xff) ^ 0xff x = (x * canvas_width) / 255 + canvas_left y = (y * canvas_height) / 255 + canvas_top (0..6).each do |dy| py = y + dy - 3 if py >= 0 && py < height (0..6).each do |dx| next if mask[dy][dx] == 0 px = x + dx - 3 if px >= 0 && px < width if pixels[py * width + px] == 0 pixels[py * width + px] = 1 end end end end end pixels[y * width + x] = (((value.to_f / histogram_max) ** 0.5) * 63).to_i end if watch[:components].size > 1 # only show X histogram if it's not cycles histogram_x.each_pair do |x, value| x = (x * canvas_width) / 255 + canvas_left normalized_value = (value.to_f / histogram_x_max * 31).to_i (0..normalized_value).each do |dy| pixels[(canvas_top - dy - 4) * width + x] = normalized_value - dy + 0x40 end end end histogram_y.each_pair do |y, value| y = ((y ^ 0xff) * canvas_height) / 255 + canvas_top normalized_value = (value.to_f / histogram_y_max * 31).to_i (0..normalized_value).each do |dx| pixels[y * width + canvas_left + canvas_width + dx + 4] |= normalized_value - dx + 0x40 end end watch[:components].each.with_index do |component, component_index| labels = [] if component[:type] == 'u8' labels << [0.0, '0'] labels << [64.0/255, '64'] labels << [128.0/255, '128'] labels << [192.0/255, '192'] labels << [1.0, '255'] elsif component[:type] == 's8' labels << [0.0, '-128'] labels << [64.0/255, '-64'] labels << [128.0/255, '0'] labels << [192.0/255, '64'] labels << [1.0, '127'] elsif component[:type] == 'u16' labels << [0.0, '0'] labels << [64.0/255, '16k'] labels << [128.0/255, '32k'] labels << [192.0/255, '48k'] labels << [1.0, '64k'] elsif component[:type] == 's16' labels << [0.0, '-32k'] labels << [64.0/255, '-16k'] labels << [128.0/255, '0'] labels << [192.0/255, '16k'] labels << [1.0, '32k'] end labels.each do |label| s = label[1] if component_index == 0 && watch[:components].size == 2 x = (label[0] * canvas_width).to_i + canvas_left print_s(pixels, width, height, (x - s.size * (6 * label[0])).to_i, canvas_top + canvas_height + 7, s, 31) (0..(canvas_height + 3)).each do |y| pixels[(y + canvas_top) * width + x] |= 0x20 end else y = ((1.0 - label[0]) * canvas_height).to_i + canvas_top print_s_r(pixels, width, height, canvas_left - 12, (y - s.size * (6 * (1.0 - label[0]))).to_i, s, 31) (-3..canvas_width).each do |x| pixels[y * width + (x + canvas_left)] |= 0x20 end end end (0..0).each do |offset| component_label = component[:name] if component_index == 0 && watch[:components].size == 2 print_s(pixels, width, height, (canvas_left + canvas_width * 0.5 - component_label.size * 3 + offset).to_i, canvas_top + canvas_height + 18, component_label, 31) else print_s_r(pixels, width, height, canvas_left - 22, (canvas_top + canvas_height * 0.5 - component_label.size * 3 + offset).to_i, component_label, 31) end end end label = "#{sprintf('0x%04x', watch[:pc])} / #{watch[:path]}:#{watch[:line_number]}" if @watch_values.include?(index) label += " (#{watch[:post] ? 'post' : 'pre'})" end print_s(pixels, width, height, width / 2 - 3 * label.size, height - 20, label, 31) if @watch_values.include?(index) label = @watch_called_from_subroutine[index].map do |x| "#{@label_for_pc[x] || sprintf('0x%04x', x)}+#{watch[:pc] - x}" end.join(', ') label = "at #{label}" print_s(pixels, width, height, width / 2 - 3 * label.size, height - 10, label, 31) end if watch[:components].size == 1 # this watch is 1D, add X axis labels for cycles labels = [] labels << [0.0, '0'] format_str = '%d' divisor = 1 if @max_cycle_count >= 1e6 format_str = '%1.1fM' divisor = 1e6 elsif @max_cycle_count > 1e3 format_str = '%1.1fk' divisor = 1e3 end # labels << [1.0, sprintf(format_str, (@max_cycle_count.to_f / divisor)).sub('.0', '')] remaining_space = canvas_width - labels.inject(0) { |a, b| a + b.size * 6 } space_per_label = sprintf(format_str, (@max_cycle_count.to_f / divisor)).sub('.0', '').size * 6 * 2 max_tween_labels = remaining_space / space_per_label step = ((@max_cycle_count / max_tween_labels).to_f / divisor).ceil step = 1 if step == 0 # prevent infinite loop! x = step while x < @max_cycle_count / divisor labels << [x.to_f * divisor / @max_cycle_count, sprintf(format_str, x).sub('.0', '')] x += step end labels.each do |label| s = label[1] x = (label[0] * canvas_width).to_i + canvas_left print_s(pixels, width, height, (x - s.size * (6 * label[0])).to_i, canvas_top + canvas_height + 7, s, 31) (0..(canvas_height + 3)).each do |y| pixels[(y + canvas_top) * width + x] |= 0x20 end end (0..0).each do |offset| component_label = 'cycles' print_s(pixels, width, height, (canvas_left + canvas_width * 0.5 - component_label.size * 3 + offset).to_i, canvas_top + canvas_height + 18, component_label, 31) end end if (!@watch_values.include?(index)) && @cycles_per_function.include?(watch[:pc]) && (!@cycles_per_function[watch[:pc]].empty?) max_cycle_count_for_function = @cycles_per_function[watch[:pc]].map do |x| x[:call_cycles] end.max # this is a subroutine cycles watch, add Y axis labels for subroutine cycles labels = [] labels << [0.0, '0'] format_str = '%d' divisor = 1 if max_cycle_count_for_function >= 1e6 format_str = '%1.1fM' divisor = 1e6 elsif max_cycle_count_for_function > 1e3 format_str = '%1.1fk' divisor = 1e3 end # labels << [1.0, sprintf(format_str, (max_cycle_count_for_function.to_f / divisor)).sub('.0', '')] remaining_space = canvas_width - labels.inject(0) { |a, b| a + b.size * 6 } space_per_label = sprintf(format_str, (max_cycle_count_for_function.to_f / divisor)).sub('.0', '').size * 6 * 2 max_tween_labels = remaining_space / space_per_label step = ((max_cycle_count_for_function / max_tween_labels).to_f / divisor).ceil step = 1 if step == 0 # prevent infinite loop! x = step while x < max_cycle_count_for_function / divisor labels << [x.to_f * divisor / max_cycle_count_for_function, sprintf(format_str, x).sub('.0', '')] x += step end labels.each do |label| s = label[1] y = ((1.0 - label[0]) * canvas_height).to_i + canvas_top print_s_r(pixels, width, height, canvas_left - 12, (y - s.size * (6 * (1.0 - label[0]))).to_i, s, 31) (-3..canvas_width).each do |x| pixels[y * width + (x + canvas_left)] |= 0x20 end end end tr = @highlight_color[1, 2].to_i(16) tg = @highlight_color[3, 2].to_i(16) tb = @highlight_color[5, 2].to_i(16) hr = @histogram_color[1, 2].to_i(16) hg = @histogram_color[3, 2].to_i(16) hb = @histogram_color[5, 2].to_i(16) if pixels colors_used = 32 * 3 gi, go, gt = Open3.popen2("./pgif #{width} #{height} #{colors_used}") palette = [0] * colors_used (0...32).each do |i| if (i == 0) r = 0xff g = 0xff b = 0xff else l = (((31 - i) + 1) << 3) - 1 r = l * tr / 255 g = l * tg / 255 b = l * tb / 255 end palette[i] = sprintf('%02x%02x%02x', r, g, b) r = r * 4 / 5 g = g * 4 / 5 b = b * 4 / 5 palette[i + 32] = sprintf('%02x%02x%02x', r, g, b) fade = (i.to_f / 31) * 0.5 + 0.5 xr = (hr * fade + 0xff * (1.0 - fade)).to_i xg = (hg * fade + 0xff * (1.0 - fade)).to_i xb = (hb * fade + 0xff * (1.0 - fade)).to_i palette[i + 64] = sprintf('%02x%02x%02x', xr, xg, xb) end gi.puts palette.join("\n") gi.puts 'f' gi.puts pixels.map { |x| sprintf('%02x', x) }.join("\n") gi.close gt.join watch_path = File.join(@files_dir, "watch_#{index}.gif") File::open(watch_path, 'w') do |f| f.write go.read end io.puts "" end else io.puts "No values recorded." end io.puts "
" end report.sub!('#{watches}', io.string) if @cycles_per_function.empty? report.sub!('#{cycle_watches}', '') else io = StringIO.new report.sub!('#{cycle_watches}', io.string) end # write cycles io = StringIO.new io.puts "" io.puts "" io.puts "" io.puts "" io.puts "" io.puts "" io.puts "" io.puts "" io.puts "" io.puts "" io.puts "" cycles_sum = @total_cycles_per_function.values.inject(0) { |a, b| a + b } @total_cycles_per_function.keys.sort do |a, b| @total_cycles_per_function[b] <=> @total_cycles_per_function[a] end.each do |pc| io.puts "" io.puts "" io.puts "" io.puts "" io.puts "" io.puts "" io.puts "" io.puts "" end io.puts "
AddrCCCC %CallsCC/CallLabel
#{sprintf('0x%04x', pc)}#{@total_cycles_per_function[pc]}#{sprintf('%1.2f%%', @total_cycles_per_function[pc].to_f * 100.0 / cycles_sum)}#{@calls_per_function[pc]}#{@total_cycles_per_function[pc] / @calls_per_function[pc]}#{@label_for_pc[pc]}
" report.sub!('#{cycles}', io.string) if @have_dot # render call graph all_nodes = Set.new() @call_graph_counts.each_pair do |key, entries| all_nodes << key entries.keys.each do |other_key| all_nodes << other_key end end io = StringIO.new io.puts "digraph {" io.puts "overlap = false;" io.puts "rankdir = LR;" io.puts "splines = true;" io.puts "graph [fontname = Arial, fontsize = 8, size = \"14, 11\", nodesep = 0.2, ranksep = 0.3, ordering = out];" io.puts "node [fontname = Arial, fontsize = 8, shape = rect, style = filled, fillcolor = \"#fce94f\" color = \"#c4a000\"];" io.puts "edge [fontname = Arial, fontsize = 8, color = \"#444444\"];" all_nodes.each do |node| label = @label_for_pc[node] || sprintf('0x%04x', node) label = "#{label}" if @calls_per_function[node] && @total_cycles_per_function[node] label += "
#{@total_cycles_per_function[node] / @calls_per_function[node]}" end io.puts " _#{node} [label = <#{label}>];" end max_call_count = 0 @call_graph_counts.each_pair do |key, entries| entries.values.each do |count| max_call_count = count if count > max_call_count end end @call_graph_counts.each_pair do |key, entries| entries.keys.each do |other_key| penwidth = 0.5 + ((entries[other_key].to_f / max_call_count) ** 0.3) * 2 io.puts "_#{key} -> _#{other_key} [label = \"#{entries[other_key]}x\", penwidth = #{penwidth}];" end end io.puts "}" dot = io.string svg = Open3.popen2('dot -Tsvg') do |stdin, stdout, thread| stdin.print(dot) stdin.close stdout.read end File::open(File.join(@files_dir, 'call_graph.svg'), 'w') do |f| f.write(svg) end report.sub!('#{call_graph}', svg) else report.sub!('#{call_graph}', '(GraphViz not installed)') end # write error dump if @error io = StringIO.new io.puts "
" io.puts "

Source code

" io.puts "
"
                source_code = @code_for_pc[@error[:pc]]
                offset = source_code[:line] - 1
                this_filename = source_code[:file]
                format_str = "%-#{@max_source_width_for_file[this_filename]}s"
                io.puts " Line |   PC   | #{sprintf(format_str, this_filename)}"
                ((offset - 16)..(offset + 16)).each do |i|
                    next if i < 0 || i >= @source_for_file[this_filename].size
                    io.print ""
                    line_pc = nil
                    if @pc_for_file_and_line[this_filename]
                        if @pc_for_file_and_line[this_filename][i + 1]
                            line_pc = sprintf('0x%04x', @pc_for_file_and_line[this_filename][i + 1])
                        end
                    end
                    line_pc ||= ''
                    io.print sprintf("%5d | %6s | %-#{@max_source_width_for_file[this_filename]}s", i + 1, line_pc, @source_for_file[this_filename][i])
                    io.print "" if i == offset
                    io.puts
                end
                io.puts "
" io.puts "
" io.puts "
" io.puts "

Execution log

" io.puts "
"
                io.puts sprintf("   PC    |    A     X     Y     PC      SP  Flags ")
                @execution_log.each do |item|
                    io.puts sprintf(" 0x%04x  |  0x%02x  0x%02x  0x%02x  0x%04x  0x%02x  0x%02x  ", *item)
                end
                io.puts sprintf(" 0x%04x  |  %-37s ", @error[:pc], @error[:message])
                io.puts "
" io.puts "
" io.puts "
" report.sub!('#{error}', io.string) else report.sub!('#{error}', '') end f.puts report end puts ' done.' end def parse_asm_int(s) if s[0] == '#' s[1, s.size - 1].to_i elsif s[0] == '$' s[1, s.size - 1].to_i(16) else s.to_i end end def parse_merlin_output(path) input_file = File.basename(@source_path) @source_for_file[input_file] = File.read(@source_path).split("\n").map { |x| x.gsub("\t", ' ' * 4) } @max_source_width_for_file[input_file] = [@source_for_file[input_file].map { |x| x.size }.max, 40].max @source_line = -3 File.open(path, 'r') do |f| f.each_line do |line| @pc_for_file_and_line[input_file] ||= {} @source_line += 1 parts = line.split('|') next unless parts.size > 2 line_type = parts[2].strip if ['Code', 'Equivalence', 'Empty'].include?(line_type) next if parts[6].nil? next unless parts[6] =~ /[0-9A-F]{2}\/[0-9A-F]{4}/ pc = parts[6].split(' ').first.split('/').last.to_i(16) code = parts[7].strip code_parts = code.split(/\s+/) line_number = parts[1].split(' ').map { |x| x.strip }.reject { |x| x.empty? }.last.to_i @code_for_pc[pc] = { :file => input_file, :line => line_number, } @pc_for_file_and_line[input_file][line_number] = pc next if code_parts.empty? label = nil champ_directives = [] if code.include?(';') comment = code[code.index(';') + 1, code.size].strip comment.scan(/@[^;][^\s]+/).each do |match| champ_directives << match.to_s end end if champ_directives.empty? && line_type == 'Equivalence' label = code_parts[0] pc = parse_asm_int(code_parts[2]) @label_for_pc[pc] = label @pc_for_label[label] = pc else unless @keywords.include?(code_parts.first) label = code_parts.first code = code.sub(label, '').strip @label_for_pc[pc] = label @pc_for_label[label] = pc end end next if champ_directives.empty? if line_type == 'Equivalence' if champ_directives.size != 1 fail('No more than one champ directive allowed in equivalence declaration.') end item = { :address => code_parts[2].sub('$', '').to_i(16), :type => parse_champ_directive(champ_directives.first, true)[:type] } @global_variables[label] = item elsif line_type == 'Code' @watches[pc] ||= [] champ_directives.each do |directive| watch = parse_champ_directive(directive, false) watch[:line_number] = line_number watch[:pc] = pc if watch[:subroutine_cycles] watch[:components].first[:name] = "#{@label_for_pc[pc] || sprintf('0x%04x', pc)} cycles" @cycles_per_function[pc] = [] end @watches[pc] << watch end end else if line.include?(';') if line.split(';').last.include?('@') fail('Champ directive not allowed here.') end end end end end end def fail(message) STDERR.puts sprintf('[%s:%d] %s', File::basename(@source_path), @source_line, message) exit(1) end def parse_champ_directive(s, global_variable = false) original_directive = s.dup # Au Au(post) As Xs(post) RX u8 Au,Xu,Yu result = {} s = s[1, s.size - 1] if s[0] == '@' result[:path] = File.basename(@source_path) if global_variable if ['u8', 's8', 'u16', 's16'].include?(s) result[:type] = s else fail("Error parsing champ directive: #{original_directive}") end else if s == 'cycles' result[:subroutine_cycles] = true result[:components] = [ {:subroutine_cycles => true} ] return result end if s.include?('(post)') result[:post] = true s.sub!('(post)', '') end result[:components] = s.split(',').map do |part| part_result = {} if ['Au', 'As', 'Xu', 'Xs', 'Yu', 'Ys'].include?(part[0, 2]) part_result[:register] = part[0] part_result[:name] = part[0] part_result[:type] = part[1] + '8' elsif @global_variables.include?(part) part_result[:address] = @global_variables[part][:address] part_result[:type] = @global_variables[part][:type] part_result[:name] = part else fail("Error parsing champ directive: #{original_directive}") exit(1) end part_result end if result[:components].size > 2 fail("No more than two components allowed per watch in #{original_directive}") end end result end end ['p65c02', 'pgif'].each do |file| unless FileUtils.uptodate?(file, ["#{file}.c"]) system("gcc -o #{file} #{file}.c") unless $?.exitstatus == 0 exit(1) end end end champ = Champ.new champ.run champ.write_report __END__ champ report #{error}

Frames

#{screenshots}

Cycles

#{cycles}

Call Graph

#{call_graph}

Watches

#{watches} #{cycle_watches}