diff --git a/README.md b/README.md index 0b4da5c..07f9492 100644 --- a/README.md +++ b/README.md @@ -36,20 +36,6 @@ We specified some source files (which will get compiled automatically) and some Furthermore, we can disable subroutines by replacing the first opcode with a RTS (`instant_rts`). This is necessary in some cases because Champ does not emulate hardware and thus can not load data from disk, for example. -### Defining watches - -You can watch registers or variables at certain program counter addresses by inserting a _champ directive_ in a comment after the respective code line in your assembler source code. All champ directives start with an _at sign_ (@). Here's an example: - -``` -LDA #2 ; load 2 into accumulator -ASL ; multiply by two -STA SOMEWHERE ; store result @Au -``` - -With the `@Au` directive, we tell champ to monitor the A register and interpret it as an unsigned 8 bit integer (likewise, `@As` would treat the value as a signed 8 bit integer). - -By default, all champ values get recorded before the operation has been executed. To get the value after the operation, you can write: `@Au(post)`. - ### Running the profiler To start champ, type: @@ -64,6 +50,105 @@ This will run the emulator and write the HTML report to `report.html`. If you do ![Champ Screenshot](doc/screenshot.png?raw=true "Fig. 1 Champ Screenshot") +### Defining watches + +You can watch registers or variables at certain program counter addresses by inserting a _champ directive_ in a comment after the respective code line in your assembler source code. All champ directives start with an _at sign_ (`@`). Here's an example ([example01.yaml](example01.yaml) / [example01.s](example01.s)): + +``` + DSK test + MX %11 + ORG $6000 + +FOO EQU $8000 + + JSR TEST + RTS + +TEST LDA #64 ; load 64 into accumulator + ASL ; multiply by two @Au + STA FOO ; store result @Au + RTS +``` + +Running this example results in the following watch graphs: + +![A at PC 0x6006](doc/example01_1.gif?raw=true) +![A at PC 0x6007](doc/example01_2.gif?raw=true) + +Champ generates a graph for every watch. You can see the watched variable plotted against the cycles, and also the PC address, file name, and source line number of the watch as well as the subroutine in which the watch was defined. + +With the `@Au` directive, we tell champ to monitor the A register and interpret it as an unsigned 8 bit integer (likewise, `@As` would treat the value as a signed 8 bit integer). + +By default, all champ values get recorded before the operation has been executed. To get the value after the operation, you can write: `@Au(post)`. Feel free to add as many champ directives as you need in a single line, each starting with a new at sign. + +### Global variables + +In addition to watching individual registers, you can also watch global variables (([example02.yaml](example02.yaml) / [example02.s](example02.s))): + +``` + DSK test + MX %11 + ORG $6000 + +FOO EQU $8000 ; @u16 + + JSR TEST + RTS + +TEST LDA #0 + STA FOO + LDA #$40 + STA FOO+1 ; @FOO(post) + RTS +``` + +Here, we declare the type of the global variable in the same place the variable itself is declared, using @u8@, @s8@, @u16@, or @s16@. Later, we can just watch the variable by issuing a champ directive like `@FOO@`. In this example, we use the `(post)` option to see the variable contents after the `STA` operation. + +![FOO at PC 0x600b](doc/example02_1.gif?raw=true) + +Here's another example with some more interesting plots ([example03.yaml](example03.yaml) / [example03.s](example03.s)): + +``` + DSK test + MX %11 + ORG $6000 + +FOO EQU $8000 + + JSR TEST + RTS + +TEST LDX #$FF + LDA #1 +LOOP TAY + TXA + EOR #$FF + LSR + LSR + STA FOO + TYA + ADC FOO ; @Au(post) + DEX ; @Xu(post) + BNE LOOP +``` + +This is a small program which lets the accumulator grow quadratically while X decreases linearly: + +![A at PC 0x6012](doc/example03_1.gif?raw=true) +![X at PC 0x6015](doc/example03_2.gif?raw=true) + +### Two-dimensional watches + +We can also watch pairs of variables by separating them with a comma in the champ directive: + +``` + DEX ; @Xu(post) @Au,FOO(post) +``` + +This will plot FOO against X: + +![FOO against A at PC 0x6015](doc/example03_3.gif?raw=true) + ## Did you know? -By the way, there's a full-fledged, incremental, standalone, no-dependencies GIF encoder in `pgif.c` that writes animated GIFs and uses some optimizations to further minimize space. It's stream-friendly and as you feed pixels in via `stdin`, it dutifully writes GIF data to `stdout` until `stdin` gets closed. +By the way, there's a full-fledged, incremental, standalone, no-dependencies GIF encoder in [pgif.c](pgif.c) that writes animated GIFs and uses some optimizations to further minimize space. It's stream-friendly and as you feed pixels in via `stdin`, it dutifully writes GIF data to `stdout` until `stdin` gets closed. diff --git a/champ.rb b/champ.rb index 79448c9..b685a09 100755 --- a/champ.rb +++ b/champ.rb @@ -68,10 +68,18 @@ class Champ exit(1) end end - @config = YAML.load(File.read(args.shift)) + config_path = args.shift + @config = YAML.load(File.read(config_path)) @source_files = [] @config['load'].each_pair do |address, path| - @source_files << {:path => File.absolute_path(path), :address => address} + 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'] @@ -363,11 +371,19 @@ class Champ histogram_max = histogram.values.max width = 276 - height = (watch[:components].size == 1) ? 286 : 286 + height = 286 canvas_top = 20 canvas_left = 40 canvas_width = width - 60 canvas_height = height - 70 + canvas_width = 256 + canvas_height = 256 + canvas_top = 10 + canvas_left = 30 + canvas_right = 10 + 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| @@ -460,6 +476,52 @@ class Champ end.join(', ') label = "at #{label}" print_s(pixels, width, height, width / 2 - 3 * label.size, height - 10, label, 63) + + 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, 63) + (0..(canvas_height + 3)).each do |y| + pixels[(y + canvas_top) * width + x] |= 0x40 + end + end + + (0..1).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, 63) + end + end tr = @highlight_color[1, 2].to_i(16) tg = @highlight_color[3, 2].to_i(16) diff --git a/doc/example01_1.gif b/doc/example01_1.gif new file mode 100644 index 0000000..1ce4913 Binary files /dev/null and b/doc/example01_1.gif differ diff --git a/doc/example01_2.gif b/doc/example01_2.gif new file mode 100644 index 0000000..0483aa4 Binary files /dev/null and b/doc/example01_2.gif differ diff --git a/doc/example02_1.gif b/doc/example02_1.gif new file mode 100644 index 0000000..03a8dd5 Binary files /dev/null and b/doc/example02_1.gif differ diff --git a/doc/example03_1.gif b/doc/example03_1.gif new file mode 100644 index 0000000..f757dc9 Binary files /dev/null and b/doc/example03_1.gif differ diff --git a/doc/example03_2.gif b/doc/example03_2.gif new file mode 100644 index 0000000..0b1508a Binary files /dev/null and b/doc/example03_2.gif differ diff --git a/doc/example03_3.gif b/doc/example03_3.gif new file mode 100644 index 0000000..1f318b5 Binary files /dev/null and b/doc/example03_3.gif differ diff --git a/examples/example01.s b/examples/example01.s new file mode 100644 index 0000000..6e8c1fd --- /dev/null +++ b/examples/example01.s @@ -0,0 +1,13 @@ + DSK test + MX %11 + ORG $6000 + +FOO EQU $8000 + + JSR TEST + RTS + +TEST LDA #64 ; load 64 into accumulator + ASL ; multiply by two @Au + STA FOO ; store result @Au + RTS diff --git a/examples/example01.yaml b/examples/example01.yaml new file mode 100644 index 0000000..c061f2e --- /dev/null +++ b/examples/example01.yaml @@ -0,0 +1,3 @@ +load: + 0x6000: example01.s +entry: 0x6000 diff --git a/examples/example02.s b/examples/example02.s new file mode 100644 index 0000000..c6ead78 --- /dev/null +++ b/examples/example02.s @@ -0,0 +1,14 @@ + DSK test + MX %11 + ORG $6000 + +FOO EQU $8000 ; @u16 + + JSR TEST + RTS + +TEST LDA #0 + STA FOO + LDA #$40 + STA FOO+1 ; @FOO(post) + RTS diff --git a/examples/example02.yaml b/examples/example02.yaml new file mode 100644 index 0000000..65bf1cc --- /dev/null +++ b/examples/example02.yaml @@ -0,0 +1,3 @@ +load: + 0x6000: example02.s +entry: 0x6000 diff --git a/examples/example03.s b/examples/example03.s new file mode 100644 index 0000000..ba58ff7 --- /dev/null +++ b/examples/example03.s @@ -0,0 +1,21 @@ + DSK test + MX %11 + ORG $6000 + +FOO EQU $8000 ; @u8 + + JSR TEST + RTS + +TEST LDX #$FF + LDA #1 +LOOP TAY + TXA + EOR #$FF + LSR + LSR + STA FOO + TYA + ADC FOO ; @Au(post) + DEX ; @Xu(post) @Au,FOO(post) + BNE LOOP diff --git a/examples/example03.yaml b/examples/example03.yaml new file mode 100644 index 0000000..ee928d2 --- /dev/null +++ b/examples/example03.yaml @@ -0,0 +1,3 @@ +load: + 0x6000: example03.s +entry: 0x6000 diff --git a/examples/plot3d.yaml b/examples/plot3d.yaml new file mode 100644 index 0000000..d0134d9 --- /dev/null +++ b/examples/plot3d.yaml @@ -0,0 +1,13 @@ +load: + 0x6000: ../plot3d/plot3d242.s + 0x1200: ../plot3d/multtab.s + 0x8400: ../plot3d/ldrwtab.s + 0x8900: ../plot3d/SINETABLE + 0x8b00: ../plot3d/object2.s + 0x9000: ../plot3d/object1.s + 0x9500: ../plot3d/FONT + 0xb600: ../plot3d/projtab.s +entry: ENTRY +# highlight: '#c3def1' +instant_rts: + - LOAD1 diff --git a/plot3d.yaml b/plot3d.yaml deleted file mode 100644 index dbf56a7..0000000 --- a/plot3d.yaml +++ /dev/null @@ -1,13 +0,0 @@ -load: - 0x6000: plot3d/plot3d242.s - 0x1200: plot3d/multtab.s - 0x8400: plot3d/ldrwtab.s - 0x8900: plot3d/SINETABLE - 0x8b00: plot3d/object2.s - 0x9000: plot3d/object1.s - 0x9500: plot3d/FONT - 0xb600: plot3d/projtab.s -entry: ENTRY -# highlight: '#c3def1' -instant_rts: - - LOAD1