about summary refs log tree commit diff stats
path: root/subx/examples/ex11.subx
blob: e2628c3947efd9e6bfbc4cfdba743136c94224b9 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
# Null-terminated vs length-prefixed ascii strings.
#
# By default we create strings with a 4-byte length prefix rather than a null suffix.
# However we still need null-prefixed strings when interacting with the Linux
# kernel in a few places. This layer implements a function for comparing
# a null-terminated 'kernel string' with a length-prefixed 'SubX string'.
#
# To run (from the subx directory):
#   $ subx translate examples/ex11.subx -o examples/ex11
#   $ subx run examples/ex11  # runs a series of tests
#   ......  # all tests pass
#
# (We can't yet run the tests when given a "test" commandline argument,
# because checking for it would require the function being tested! Breakage
# would cause tests to not run, rather than to fail as we'd like.)

== code
#   instruction                     effective address                                                   register    displacement    immediate
# . op          subop               mod             rm32          base        index         scale       r32
# . 1-3 bytes   3 bits              2 bits          3 bits        3 bits      3 bits        2 bits      2 bits      0/1/2/4 bytes   0/1/2/4 bytes

# main:
    e8/call  run-tests/disp32  # 'run-tests' is a function created automatically by SubX. It calls all functions that start with 'test-'.
    # syscall(exit, EAX)
    89/copy                         3/mod/direct    3/rm32/EBX    .           .             .           0/r32/EAX   .               .                 # copy EAX to EBX
    b8/copy-to-EAX  1/imm32
    cd/syscall  0x80/imm8

# compare a null-terminated ascii string with a more idiomatic length-prefixed byte array
# reason for the name: the only place we should have null-terminated ascii strings is from commandline args
kernel-string-equal:  # s : null-terminated ascii string, benchmark : length-prefixed ascii string -> EAX : boolean
    # . prolog
    55/push-EBP
    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
    # . save registers
    51/push-ECX
    52/push-EDX
    53/push-EBX
    56/push-ESI
    57/push-EDI

    # pseudocode:
    #   initialize n = b.length
    #   initialize s1 = s
    #   initialize s2 = b.data
    #   i = 0
    #   for (i = 0; i < n; ++n)
    #     c1 = *s1
    #     c2 = *s2
    #     if c1 == 0
    #       return false
    #     if c1 != c2
    #       return false
    #   return *s1 == 0
    # initialize s into EDI
    8b/copy                         1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           7/r32/EDI   8/disp8         .                 # copy *(EBP+8) to EDI
    # initialize benchmark length n into EDX
    8b/copy                         1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           2/r32/EDX   0xc/disp8       .                 # copy *(EBP+12) to EDX
    8b/copy                         0/mod/indirect  2/rm32/EDX    .           .             .           2/r32/EDX   .               .                 # copy *EDX to EDX
    # initialize benchmark data into ESI
    8b/copy                         1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           6/r32/ESI   0xc/disp8       .                 # copy *(EBP+12) to ESI
    81          0/subop/add         3/mod/direct    6/rm32/ESI    .           .             .           .           .               4/imm32           # add to ESI
    # initialize loop counter i into ECX
    b9/copy-to-ECX  0/imm32/exit
    # while (i/ECX < n/EDX)
$kernel-string-equal:loop:
    39/compare                      3/mod/direct    1/rm32/ECX    .           .             .           2/r32/EDX   .               .                 # compare ECX with EDX
    74/jump-if-equal  $kernel-string-equal:break/disp8
    # c1/EAX, c2/EBX = *s, *benchmark
    b8/copy-to-EAX  0/imm32
    8a/copy                         0/mod/indirect  7/rm32/EDI    .           .             .           0/r32/EAX   .               .                 # copy byte at *EDI to lower byte of EAX
    bb/copy-to-EBX  0/imm32
    8a/copy                         0/mod/indirect  6/rm32/ESI    .           .             .           3/r32/EBX   .               .                 # copy byte at *ESI to lower byte of EBX
    # if (c1 == 0) return false
    3d/compare-EAX  0/imm32
    74/jump-if-equal  $kernel-string-equal:false/disp8
    # if (c1 != c2) return false
    39/compare                      3/mod/direct    0/rm32/EAX    .           .             .           3/r32/EBX   .               .                 # compare EAX with EBX
    75/jump-if-not-equal  $kernel-string-equal:false/disp8
    # ++s1, ++s2, ++i
    41/inc-ECX
    46/inc-ESI
    47/inc-EDI
    # end while
    eb/jump  $kernel-string-equal:loop/disp8
$kernel-string-equal:break:
    # if (*s/EDI == 0) return true
    b8/copy-to-EAX  0/imm32
    8a/copy                         0/mod/indirect  7/rm32/EDI    .           .             .           0/r32/EAX   .               .                 # copy byte at *EDI to lower byte of EAX
    81          7/subop/compare     3/mod/direct    0/rm32/EAX    .           .             .           .           .               0/imm32           # compare EAX
    75/jump-if-not-equal  $kernel-string-equal:false/disp8
    b8/copy-to-EAX  1/imm32
$kernel-string-equal:true:
    eb/jump  $kernel-string-equal:end/disp8
    # return false
$kernel-string-equal:false:
    b8/copy-to-EAX  0/imm32

$kernel-string-equal:end:
    # . restore registers
    5f/pop-to-EDI
    5e/pop-to-ESI
    5b/pop-to-EBX
    5a/pop-to-EDX
    59/pop-to-ECX
    # end
    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
    5d/pop-to-EBP
    c3/return

# - tests

test-compare-null-kernel-string-with-empty-array:
    # EAX = kernel-string-equal(Null-kernel-string, "")
    # . . push args
    68/push  ""/imm32
    68/push  Null-kernel-string/imm32
    # . . call
    e8/call  kernel-string-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # check-ints-equal(EAX, 1, msg)
    # . . push args
    68/push  "F - test-compare-null-kernel-string-with-empty-array"/imm32
    68/push  1/imm32/true
    50/push-EAX
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
    c3/return

test-compare-null-kernel-string-with-non-empty-array:
    # EAX = kernel-string-equal(Null-kernel-string, "Abc")
    # . . push args
    68/push  "Abc"/imm32
    68/push  Null-kernel-string/imm32
    # . . call
    e8/call  kernel-string-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # check-ints-equal(EAX, 0, msg)
    # . . push args
    68/push  "F - test-compare-null-kernel-string-with-non-empty-array"/imm32
    68/push  0/imm32/false
    50/push-EAX
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
    c3/return

test-compare-kernel-string-with-equal-array:
    # EAX = kernel-string-equal(Abc-kernel-string, "Abc")
    # . . push args
    68/push  "Abc"/imm32
    68/push  Abc-kernel-string/imm32
    # . . call
    e8/call  kernel-string-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # check-ints-equal(EAX, 1, msg)
    # . . push args
    68/push  "F - test-compare-kernel-string-with-equal-array"/imm32
    68/push  1/imm32/true
    50/push-EAX
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
    c3/return

test-compare-kernel-string-with-inequal-array:
    # EAX = kernel-string-equal(Abc-kernel-string, "Adc")
    # . . push args
    68/push  "Adc"/imm32
    68/push  Abc-kernel-string/imm32
    # . . call
    e8/call  kernel-string-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # check-ints-equal(EAX, 0, msg)
    # . . push args
    68/push  "F - test-compare-kernel-string-with-equal-array"/imm32
    68/push  0/imm32/false
    50/push-EAX
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
    c3/return

test-compare-kernel-string-with-empty-array:
    # EAX = kernel-string-equal(Abc-kernel-string, "")
    # . . push args
    68/push  ""/imm32
    68/push  Abc-kernel-string/imm32
    # . . call
    e8/call  kernel-string-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # check-ints-equal(EAX, 0)
    # . . push args
    68/push  "F - test-compare-kernel-string-with-equal-array"/imm32
    68/push  0/imm32/false
    50/push-EAX
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
    c3/return

test-compare-kernel-string-with-shorter-array:
    # EAX = kernel-string-equal(Abc-kernel-string, "Ab")
    # . . push args
    68/push  "Ab"/imm32
    68/push  Abc-kernel-string/imm32
    # . . call
    e8/call  kernel-string-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # check-ints-equal(EAX, 0)
    # . . push args
    68/push  "F - test-compare-kernel-string-with-shorter-array"/imm32
    68/push  0/imm32/false
    50/push-EAX
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
    c3/return

test-compare-kernel-string-with-longer-array:
    # EAX = kernel-string-equal(Abc-kernel-string, "Abcd")
    # . . push args
    68/push  "Abcd"/imm32
    68/push  Abc-kernel-string/imm32
    # . . call
    e8/call  kernel-string-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # check-ints-equal(EAX, 0)
    # . . push args
    68/push  "F - test-compare-kernel-string-with-longer-array"/imm32
    68/push  0/imm32/false
    50/push-EAX
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
    c3/return

# - helpers

# print msg to stderr if a != b, otherwise print "."
check-ints-equal:  # (a : int, b : int, msg : (address array byte)) -> boolean
    # . prolog
    55/push-EBP
    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
    # . save registers
    51/push-ECX
    53/push-EBX
    # load args into EAX, EBX and ECX
    8b/copy                         1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           0/r32/EAX   0x8/disp8       .                 # copy *(EBP+8) to EAX
    8b/copy                         1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           3/r32/EBX   0xc/disp8       .                 # copy *(EBP+12) to EBX
    # if EAX == b/EBX print('.') and return
    39/compare                      3/mod/direct    0/rm32/EAX    .           .             .           3/r32/EBX   .               .                 # compare EAX and EBX
    75/jump-if-unequal  $check-ints-equal:else/disp8
    # . write-stderr('.')
    # . . push args
    68/push  "."/imm32
    # . . call
    e8/call  write-stderr/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # . return
    eb/jump  $check-ints-equal:end/disp8
    # otherwise print(msg)
$check-ints-equal:else:
    # copy msg into ECX
    8b/copy                         1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           1/r32/ECX   0x10/disp8       .                # copy *(EBP+16) to ECX
    # print(ECX)
    # . . push args
    51/push-ECX
    # . . call
    e8/call  write-stderr/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # print newline
    # . . push args
    68/push  Newline/imm32
    # . . call
    e8/call  write-stderr/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
$check-ints-equal:end:
    # . restore registers
    5b/pop-to-EBX
    59/pop-to-ECX
    # end
    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
    5d/pop-to-EBP
    c3/return

write-stderr:  # s : (address array byte) -> <void>
    # . prolog
    55/push-EBP
    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
    # . save registers
    50/push-EAX
    51/push-ECX
    52/push-EDX
    53/push-EBX
    # syscall(write, 2/stderr, (data) s+4, (size) *s)
    # . . fd = 2 (stderr)
    bb/copy-to-EBX  2/imm32
    # . . x = s+4
    8b/copy                         1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           1/r32/ECX   8/disp8         .                 # copy *(EBP+8) to ECX
    81          0/subop/add         3/mod/direct    1/rm32/ECX    .           .             .           .           .               4/imm32           # add to ECX
    # . . size = *s
    8b/copy                         1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           2/r32/EDX   8/disp8         .                 # copy *(EBP+8) to EDX
    8b/copy                         0/mod/indirect  2/rm32/EDX    .           .             .           2/r32/EDX   .               .                 # copy *EDX to EDX
    # . . syscall
    b8/copy-to-EAX  4/imm32/write
    cd/syscall  0x80/imm8
    # . restore registers
    5b/pop-to-EBX
    5a/pop-to-EDX
    59/pop-to-ECX
    58/pop-to-EAX
    # . end
    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
    5d/pop-to-EBP
    c3/return

== data

Newline:
    # size
    01 00 00 00
    # data
    0a/newline

# for kernel-string-equal tests
Null-kernel-string:
    00/null
Abc-kernel-string:
    41/A 62/b 63/c 00/null

# . . vim:nowrap:textwidth=0
a id='n1011' href='#n1011'>1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120
# Loading images from disk, rendering images to screen.
#
# Currently supports ASCII Netpbm formats.
#   https://en.wikipedia.org/wiki/Netpbm#File_formats

type image {
  type: int  # supported types:
             #  1: portable bitmap (P1) - pixels 0 or 1
             #  2: portable greymap (P2) - pixels 1-byte greyscale values
             #  3: portable pixmap (P3) - pixels 3-byte rgb values
  max: int
  width: int
  height: int
  data: (handle array byte)
}

fn initialize-image _self: (addr image), in: (addr stream byte) {
  var self/esi: (addr image) <- copy _self
  var mode-storage: slice
  var mode/ecx: (addr slice) <- address mode-storage
  next-word-skipping-comments in, mode
  {
    var P1?/eax: boolean <- slice-equal? mode, "P1"
    compare P1?, 0/false
    break-if-=
    var type-a/eax: (addr int) <- get self, type
    copy-to *type-a, 1/ppm
    initialize-image-from-pbm self, in
    return
  }
  {
    var P2?/eax: boolean <- slice-equal? mode, "P2"
    compare P2?, 0/false
    break-if-=
    var type-a/eax: (addr int) <- get self, type
    copy-to *type-a, 2/pgm
    initialize-image-from-pgm self, in
    return
  }
  {
    var P3?/eax: boolean <- slice-equal? mode, "P3"
    compare P3?, 0/false
    break-if-=
    var type-a/eax: (addr int) <- get self, type
    copy-to *type-a, 3/ppm
    initialize-image-from-ppm self, in
    return
  }
  abort "initialize-image: unrecognized image type"
}

# dispatch to a few variants with mostly identical boilerplate
# TODO: if we have more resolution we could actually use it to improve
# dithering
fn render-image screen: (addr screen), _img: (addr image), xmin: int, ymin: int, width: int, height: int {
  var img/esi: (addr image) <- copy _img
  var type-a/eax: (addr int) <- get img, type
  {
    compare *type-a, 1/pbm
    break-if-!=
    render-pbm-image screen, img, xmin, ymin, width, height
    return
  }
  {
    compare *type-a, 2/pgm
    break-if-!=
    var img2-storage: image
    var img2/edi: (addr image) <- address img2-storage
    dither-pgm-unordered img, img2
    render-raw-image screen, img2, xmin, ymin, width, height
    return
  }
  {
    compare *type-a, 3/ppm
    break-if-!=
    var img2-storage: image
    var img2/edi: (addr image) <- address img2-storage
    dither-ppm-unordered img, img2
    render-raw-image screen, img2, xmin, ymin, width, height
    return
  }
#?   abort "render-image: unrecognized image type"
}

## helpers

# import a black-and-white ascii bitmap (each pixel is 0 or 1)
fn initialize-image-from-pbm _self: (addr image), in: (addr stream byte) {
  var self/esi: (addr image) <- copy _self
  var curr-word-storage: slice
  var curr-word/ecx: (addr slice) <- address curr-word-storage
  # load width, height
  next-word-skipping-comments in, curr-word
  var tmp/eax: int <- parse-decimal-int-from-slice curr-word
  var width/edx: int <- copy tmp
  next-word-skipping-comments in, curr-word
  tmp <- parse-decimal-int-from-slice curr-word
  var height/ebx: int <- copy tmp
  # save width, height
  var dest/eax: (addr int) <- get self, width
  copy-to *dest, width
  dest <- get self, height
  copy-to *dest, height
  # initialize data
  var capacity/edx: int <- copy width
  capacity <- multiply height
  var data-ah/edi: (addr handle array byte) <- get self, data
  populate data-ah, capacity
  var _data/eax: (addr array byte) <- lookup *data-ah
  var data/edi: (addr array byte) <- copy _data
  var i/ebx: int <- copy 0
  {
    compare i, capacity
    break-if->=
    next-word-skipping-comments in, curr-word
    var src/eax: int <- parse-decimal-int-from-slice curr-word
    {
      var dest/ecx: (addr byte) <- index data, i
      copy-byte-to *dest, src
    }
    i <- increment
    loop
  }
}

# render a black-and-white ascii bitmap (each pixel is 0 or 1)
fn render-pbm-image screen: (addr screen), _img: (addr image), xmin: int, ymin: int, width: int, height: int {
  var img/esi: (addr image) <- copy _img
  # yratio = height/img->height
  var img-height-a/eax: (addr int) <- get img, height
  var img-height/xmm0: float <- convert *img-height-a
  var yratio/xmm1: float <- convert height
  yratio <- divide img-height
  # xratio = width/img->width
  var img-width-a/eax: (addr int) <- get img, width
  var img-width/ebx: int <- copy *img-width-a
  var img-width-f/xmm0: float <- convert img-width
  var xratio/xmm2: float <- convert width
  xratio <- divide img-width-f
  # esi = img->data
  var img-data-ah/eax: (addr handle array byte) <- get img, data
  var _img-data/eax: (addr array byte) <- lookup *img-data-ah
  var img-data/esi: (addr array byte) <- copy _img-data
  var len/edi: int <- length img-data
  #
  var one/eax: int <- copy 1
  var one-f/xmm3: float <- convert one
  var width-f/xmm4: float <- convert width
  var height-f/xmm5: float <- convert height
  var zero/eax: int <- copy 0
  var zero-f/xmm0: float <- convert zero
  var y/xmm6: float <- copy zero-f
  {
    compare y, height-f
    break-if-float>=
    var imgy-f/xmm5: float <- copy y
    imgy-f <- divide yratio
    var imgy/edx: int <- truncate imgy-f
    var x/xmm7: float <- copy zero-f
    {
      compare x, width-f
      break-if-float>=
      var imgx-f/xmm5: float <- copy x
      imgx-f <- divide xratio
      var imgx/ecx: int <- truncate imgx-f
      var idx/eax: int <- copy imgy
      idx <- multiply img-width
      idx <- add imgx
      # error info in case we rounded wrong and 'index' will fail bounds-check
      compare idx, len
      {
        break-if-<
        set-cursor-position 0/screen, 0x20/x 0x20/y
        draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, imgx, 3/fg 0/bg
        draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, imgy, 4/fg 0/bg
        draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, idx, 5/fg 0/bg
      }
      var src-a/eax: (addr byte) <- index img-data, idx
      var src/eax: byte <- copy-byte *src-a
      var color-int/eax: int <- copy src
      {
        compare color-int, 0/black
        break-if-=
        color-int <- copy 0xf/white
      }
      var screenx/ecx: int <- convert x
      screenx <- add xmin
      var screeny/edx: int <- convert y
      screeny <- add ymin
      pixel screen, screenx, screeny, color-int
      x <- add one-f
      loop
    }
    y <- add one-f
    loop
  }
}

# import a greyscale ascii "greymap" (each pixel is a shade of grey from 0 to 255)
fn initialize-image-from-pgm _self: (addr image), in: (addr stream byte) {
  var self/esi: (addr image) <- copy _self
  var curr-word-storage: slice
  var curr-word/ecx: (addr slice) <- address curr-word-storage
  # load width, height
  next-word-skipping-comments in, curr-word
  var tmp/eax: int <- parse-decimal-int-from-slice curr-word
  var width/edx: int <- copy tmp
  next-word-skipping-comments in, curr-word
  tmp <- parse-decimal-int-from-slice curr-word
  var height/ebx: int <- copy tmp
  # check and save color levels
  next-word-skipping-comments in, curr-word
  {
    tmp <- parse-decimal-int-from-slice curr-word
    compare tmp, 0xff
    break-if-=
    draw-text-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, "levels of grey is not 255; continuing and hoping for the best", 0x2b/fg 0/bg
  }
  var dest/edi: (addr int) <- get self, max
  copy-to *dest, tmp
  # save width, height
  dest <- get self, width
  copy-to *dest, width
  dest <- get self, height
  copy-to *dest, height
  # initialize data
  var capacity/edx: int <- copy width
  capacity <- multiply height
  var data-ah/edi: (addr handle array byte) <- get self, data
  populate data-ah, capacity
  var _data/eax: (addr array byte) <- lookup *data-ah
  var data/edi: (addr array byte) <- copy _data
  var i/ebx: int <- copy 0
  {
    compare i, capacity
    break-if->=
    next-word-skipping-comments in, curr-word
    var src/eax: int <- parse-decimal-int-from-slice curr-word
    {
      var dest/ecx: (addr byte) <- index data, i
      copy-byte-to *dest, src
    }
    i <- increment
    loop
  }
}

# render a greyscale ascii "greymap" (each pixel is a shade of grey from 0 to 255) by quantizing the shades
fn render-pgm-image screen: (addr screen), _img: (addr image), xmin: int, ymin: int, width: int, height: int {
  var img/esi: (addr image) <- copy _img
  # yratio = height/img->height
  var img-height-a/eax: (addr int) <- get img, height
  var img-height/xmm0: float <- convert *img-height-a
  var yratio/xmm1: float <- convert height
  yratio <- divide img-height
  # xratio = width/img->width
  var img-width-a/eax: (addr int) <- get img, width
  var img-width/ebx: int <- copy *img-width-a
  var img-width-f/xmm0: float <- convert img-width
  var xratio/xmm2: float <- convert width
  xratio <- divide img-width-f
  # esi = img->data
  var img-data-ah/eax: (addr handle array byte) <- get img, data
  var _img-data/eax: (addr array byte) <- lookup *img-data-ah
  var img-data/esi: (addr array byte) <- copy _img-data
  var len/edi: int <- length img-data
  #
  var one/eax: int <- copy 1
  var one-f/xmm3: float <- convert one
  var width-f/xmm4: float <- convert width
  var height-f/xmm5: float <- convert height
  var zero/eax: int <- copy 0
  var zero-f/xmm0: float <- convert zero
  var y/xmm6: float <- copy zero-f
  {
    compare y, height-f
    break-if-float>=
    var imgy-f/xmm5: float <- copy y
    imgy-f <- divide yratio
    var imgy/edx: int <- truncate imgy-f
    var x/xmm7: float <- copy zero-f
    {
      compare x, width-f
      break-if-float>=
      var imgx-f/xmm5: float <- copy x
      imgx-f <- divide xratio
      var imgx/ecx: int <- truncate imgx-f
      var idx/eax: int <- copy imgy
      idx <- multiply img-width
      idx <- add imgx
      # error info in case we rounded wrong and 'index' will fail bounds-check
      compare idx, len
      {
        break-if-<
        set-cursor-position 0/screen, 0x20/x 0x20/y
        draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, imgx, 3/fg 0/bg
        draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, imgy, 4/fg 0/bg
        draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, idx, 5/fg 0/bg
      }
      var src-a/eax: (addr byte) <- index img-data, idx
      var src/eax: byte <- copy-byte *src-a
      var color-int/eax: int <- nearest-grey src
      var screenx/ecx: int <- convert x
      screenx <- add xmin
      var screeny/edx: int <- convert y
      screeny <- add ymin
      pixel screen, screenx, screeny, color-int
      x <- add one-f
      loop
    }
    y <- add one-f
    loop
  }
}

fn nearest-grey level-255: byte -> _/eax: int {
  var result/eax: int <- copy level-255
  result <- shift-right 4
  result <- add 0x10
  return result
}

fn dither-pgm-unordered-monochrome _src: (addr image), _dest: (addr image) {
  var src/esi: (addr image) <- copy _src
  var dest/edi: (addr image) <- copy _dest
  # copy 'width'
  var src-width-a/eax: (addr int) <- get src, width
  var tmp/eax: int <- copy *src-width-a
  var src-width: int
  copy-to src-width, tmp
  {
    var dest-width-a/edx: (addr int) <- get dest, width
    copy-to *dest-width-a, tmp
  }
  # copy 'height'
  var src-height-a/eax: (addr int) <- get src, height
  var tmp/eax: int <- copy *src-height-a
  var src-height: int
  copy-to src-height, tmp
  {
    var dest-height-a/ecx: (addr int) <- get dest, height
    copy-to *dest-height-a, tmp
  }
  # transform 'data'
  var capacity/ebx: int <- copy src-width
  capacity <- multiply src-height
  var dest/edi: (addr image) <- copy _dest
  var dest-data-ah/eax: (addr handle array byte) <- get dest, data
  populate dest-data-ah, capacity
  var _dest-data/eax: (addr array byte) <- lookup *dest-data-ah
  var dest-data/edi: (addr array byte) <- copy _dest-data
  # needs a buffer to temporarily hold more than 256 levels of precision
  var errors-storage: (array int 0xc0000)
  var errors/ebx: (addr array int) <- address errors-storage
  var src-data-ah/eax: (addr handle array byte) <- get src, data
  var _src-data/eax: (addr array byte) <- lookup *src-data-ah
  var src-data/esi: (addr array byte) <- copy _src-data
  var y/edx: int <- copy 0
  {
    compare y, src-height
    break-if->=
    var x/ecx: int <- copy 0
    {
      compare x, src-width
      break-if->=
      var curr/eax: byte <- _read-pgm-buffer src-data, x, y, src-width
      var curr-int/eax: int <- copy curr
      curr-int <- shift-left 0x10  # we have 32 bits; we'll use 16 bits for the fraction and leave 8 for unanticipated overflow
      var error/esi: int <- _read-dithering-error errors, x, y, src-width
      error <- add curr-int
      $_dither-pgm-unordered-monochrome:update-error: {
        compare error, 0x800000
        {
          break-if->=
          _write-raw-buffer dest-data, x, y, src-width, 0/black
          break $_dither-pgm-unordered-monochrome:update-error
        }
        _write-raw-buffer dest-data, x, y, src-width, 1/white
        error <- subtract 0xff0000
      }
      _diffuse-dithering-error-floyd-steinberg errors, x, y, src-width, src-height, error
      x <- increment
      loop
    }
    move-cursor-to-left-margin-of-next-line 0/screen
    y <- increment
    loop
  }
}

fn dither-pgm-unordered _src: (addr image), _dest: (addr image) {
  var src/esi: (addr image) <- copy _src
  var dest/edi: (addr image) <- copy _dest
  # copy 'width'
  var src-width-a/eax: (addr int) <- get src, width
  var tmp/eax: int <- copy *src-width-a
  var src-width: int
  copy-to src-width, tmp
  {
    var dest-width-a/edx: (addr int) <- get dest, width
    copy-to *dest-width-a, tmp
  }
  # copy 'height'
  var src-height-a/eax: (addr int) <- get src, height
  var tmp/eax: int <- copy *src-height-a
  var src-height: int
  copy-to src-height, tmp
  {
    var dest-height-a/ecx: (addr int) <- get dest, height
    copy-to *dest-height-a, tmp
  }
  # compute scaling factor 255/max
  var target-scale/eax: int <- copy 0xff
  var scale-f/xmm7: float <- convert target-scale
  var src-max-a/eax: (addr int) <- get src, max
  var tmp-f/xmm0: float <- convert *src-max-a
  scale-f <- divide tmp-f
  # transform 'data'
  var capacity/ebx: int <- copy src-width
  capacity <- multiply src-height
  var dest/edi: (addr image) <- copy _dest
  var dest-data-ah/eax: (addr handle array byte) <- get dest, data
  populate dest-data-ah, capacity
  var _dest-data/eax: (addr array byte) <- lookup *dest-data-ah
  var dest-data/edi: (addr array byte) <- copy _dest-data
  # needs a buffer to temporarily hold more than 256 levels of precision
  var errors-storage: (array int 0xc0000)
  var errors/ebx: (addr array int) <- address errors-storage
  var src-data-ah/eax: (addr handle array byte) <- get src, data
  var _src-data/eax: (addr array byte) <- lookup *src-data-ah
  var src-data/esi: (addr array byte) <- copy _src-data
  var y/edx: int <- copy 0
  {
    compare y, src-height
    break-if->=
    var x/ecx: int <- copy 0
    {
      compare x, src-width
      break-if->=
      var initial-color/eax: byte <- _read-pgm-buffer src-data, x, y, src-width
      # . scale to 255 levels
      var initial-color-int/eax: int <- copy initial-color
      var initial-color-f/xmm0: float <- convert initial-color-int
      initial-color-f <- multiply scale-f
      initial-color-int <- convert initial-color-f
      var error/esi: int <- _read-dithering-error errors, x, y, src-width
      # error += (initial-color << 16)
      {
        var tmp/eax: int <- copy initial-color-int
        tmp <- shift-left 0x10  # we have 32 bits; we'll use 16 bits for the fraction and leave 8 for unanticipated overflow
        error <- add tmp
      }
      # nearest-color = nearest(error >> 16)
      var nearest-color/eax: int <- copy error
      nearest-color <- shift-right-signed 0x10
      {
        compare nearest-color, 0
        break-if->=
        nearest-color <- copy 0
      }
      {
        compare nearest-color, 0xf0
        break-if-<=
        nearest-color <- copy 0xf0
      }
      # . truncate last 4 bits
      nearest-color <- and 0xf0
      # error -= (nearest-color << 16)
      {
        var tmp/eax: int <- copy nearest-color
        tmp <- shift-left 0x10
        error <- subtract tmp
      }
      # color-index = (nearest-color >> 4 + 16)
      var color-index/eax: int <- copy nearest-color
      color-index <- shift-right 4
      color-index <- add 0x10
      var color-index-byte/eax: byte <- copy-byte color-index
      _write-raw-buffer dest-data, x, y, src-width, color-index-byte
      _diffuse-dithering-error-floyd-steinberg errors, x, y, src-width, src-height, error
      x <- increment
      loop
    }
    y <- increment
    loop
  }
}

# Use Floyd-Steinberg algorithm for diffusing error at x, y in a 2D grid of
# dimensions (width, height)
#
# https://tannerhelland.com/2012/12/28/dithering-eleven-algorithms-source-code.html
#
# Error is currently a fixed-point number with 16-bit fraction. But
# interestingly this function doesn't care about that.
fn _diffuse-dithering-error-floyd-steinberg errors: (addr array int), x: int, y: int, width: int, height: int, error: int {
  {
    compare error, 0
    break-if-!=
    return
  }
  var width-1/esi: int <- copy width
  width-1 <- decrement
  var height-1/edi: int <- copy height
  height-1 <- decrement
  # delta = error/16
#?   show-errors errors, width, height, x, y
  var delta/ecx: int <- copy error
  delta <- shift-right-signed 4
  # In Floyd-Steinberg, each pixel X transmits its errors to surrounding
  # pixels in the following proportion:
  #           X     7/16
  #     3/16  5/16  1/16
  var x/edx: int <- copy x
  {
    compare x, width-1
    break-if->=
    var tmp/eax: int <- copy 7
    tmp <- multiply delta
    var xright/edx: int <- copy x
    xright <- increment
    _accumulate-dithering-error errors, xright, y, width, tmp
  }
  var y/ebx: int <- copy y
  {
    compare y, height-1
    break-if-<
    return
  }
  var ybelow: int
  copy-to ybelow, y
  increment ybelow
  {
    compare x, 0
    break-if-<=
    var tmp/eax: int <- copy 3
    tmp <- multiply delta
    var xleft/edx: int <- copy x
    xleft <- decrement
    _accumulate-dithering-error errors, xleft, ybelow, width, tmp
  }
  {
    var tmp/eax: int <- copy 5
    tmp <- multiply delta
    _accumulate-dithering-error errors, x, ybelow, width, tmp
  }
  {
    compare x, width-1
    break-if->=
    var xright/edx: int <- copy x
    xright <- increment
    _accumulate-dithering-error errors, xright, ybelow, width, delta
  }
#?   show-errors errors, width, height, x, y
}

fn _accumulate-dithering-error errors: (addr array int), x: int, y: int, width: int, error: int {
  var curr/esi: int <- _read-dithering-error errors, x, y, width
  curr <- add error
  _write-dithering-error errors, x, y, width, curr
}

fn _read-dithering-error _errors: (addr array int), x: int, y: int, width: int -> _/esi: int {
  var errors/esi: (addr array int) <- copy _errors
  var idx/ecx: int <- copy y
  idx <- multiply width
  idx <- add x
  var result-a/eax: (addr int) <- index errors, idx
  return *result-a
}

fn _write-dithering-error _errors: (addr array int), x: int, y: int, width: int, val: int {
  var errors/esi: (addr array int) <- copy _errors
  var idx/ecx: int <- copy y
  idx <- multiply width
  idx <- add x
#?   draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, idx, 7/fg 0/bg
#?   move-cursor-to-left-margin-of-next-line 0/screen
  var src/eax: int <- copy val
  var dest-a/edi: (addr int) <- index errors, idx
  copy-to *dest-a, src
}

fn _read-pgm-buffer _buf: (addr array byte), x: int, y: int, width: int -> _/eax: byte {
  var buf/esi: (addr array byte) <- copy _buf
  var idx/ecx: int <- copy y
  idx <- multiply width
  idx <- add x
  var result-a/eax: (addr byte) <- index buf, idx
  var result/eax: byte <- copy-byte *result-a
  return result
}

fn _write-raw-buffer _buf: (addr array byte), x: int, y: int, width: int, val: byte {
  var buf/esi: (addr array byte) <- copy _buf
  var idx/ecx: int <- copy y
  idx <- multiply width
  idx <- add x
  var src/eax: byte <- copy val
  var dest-a/edi: (addr byte) <- index buf, idx
  copy-byte-to *dest-a, src
}

# some debugging helpers
fn show-errors errors: (addr array int), width: int, height: int, x: int, y: int {
  compare y, 1
  {
    break-if-=
    return
  }
  compare x, 0
  {
    break-if-=
    return
  }
  var y/edx: int <- copy 0
  {
    compare y, height
    break-if->=
    var x/ecx: int <- copy 0
    {
      compare x, width
      break-if->=
      var error/esi: int <- _read-dithering-error errors, x, y, width
      psd "e", error, 5/fg, x, y
      x <- increment
      loop
    }
    move-cursor-to-left-margin-of-next-line 0/screen
    y <- increment
    loop
  }
}

fn psd s: (addr array byte), d: int, fg: int, x: int, y: int {
  {
    compare y, 0x18
    break-if->=
    return
  }
  {
    compare y, 0x1c
    break-if-<=
    return
  }
  {
    compare x, 0x40
    break-if->=
    return
  }
#?   {
#?     compare x, 0x48
#?     break-if-<=
#?     return
#?   }
  draw-text-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, s, 7/fg 0/bg
  draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, d, fg 0/bg
}

fn psx s: (addr array byte), d: int, fg: int, x: int, y: int {
#?   {
#?     compare y, 0x60
#?     break-if->=
#?     return
#?   }
#?   {
#?     compare y, 0x6c
#?     break-if-<=
#?     return
#?   }
  {
    compare x, 0x20
    break-if->=
    return
  }
#?   {
#?     compare x, 0x6c
#?     break-if-<=
#?     return
#?   }
  draw-text-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, s, 7/fg 0/bg
  draw-int32-hex-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, d, fg 0/bg
}

# import a color ascii "pixmap" (each pixel consists of 3 shades of r/g/b from 0 to 255)
fn initialize-image-from-ppm _self: (addr image), in: (addr stream byte) {
  var self/esi: (addr image) <- copy _self
  var curr-word-storage: slice
  var curr-word/ecx: (addr slice) <- address curr-word-storage
  # load width, height
  next-word-skipping-comments in, curr-word
  var tmp/eax: int <- parse-decimal-int-from-slice curr-word
  var width/edx: int <- copy tmp
  next-word-skipping-comments in, curr-word
  tmp <- parse-decimal-int-from-slice curr-word
  var height/ebx: int <- copy tmp
  next-word-skipping-comments in, curr-word
  # check color levels
  {
    tmp <- parse-decimal-int-from-slice curr-word
    compare tmp, 0xff
    break-if-=
    abort "initialize-image-from-ppm: supports exactly 255 levels per rgb channel"
  }
  var dest/edi: (addr int) <- get self, max
  copy-to *dest, tmp
  # save width, height
  dest <- get self, width
  copy-to *dest, width
  dest <- get self, height
  copy-to *dest, height
  # initialize data
  var capacity/edx: int <- copy width
  capacity <- multiply height
  # . multiply by 3 for the r/g/b channels
  var tmp/eax: int <- copy capacity
  tmp <- shift-left 1
  capacity <- add tmp
  #
  var data-ah/edi: (addr handle array byte) <- get self, data
  populate data-ah, capacity
  var _data/eax: (addr array byte) <- lookup *data-ah
  var data/edi: (addr array byte) <- copy _data
  var i/ebx: int <- copy 0
  {
    compare i, capacity
    break-if->=
    next-word-skipping-comments in, curr-word
    var src/eax: int <- parse-decimal-int-from-slice curr-word
    {
      var dest/ecx: (addr byte) <- index data, i
      copy-byte-to *dest, src
    }
    i <- increment
    loop
  }
}

# import a color ascii "pixmap" (each pixel consists of 3 shades of r/g/b from 0 to 255)
fn render-ppm-image screen: (addr screen), _img: (addr image), xmin: int, ymin: int, width: int, height: int {
  var img/esi: (addr image) <- copy _img
  # yratio = height/img->height
  var img-height-a/eax: (addr int) <- get img, height
  var img-height/xmm0: float <- convert *img-height-a
  var yratio/xmm1: float <- convert height
  yratio <- divide img-height
  # xratio = width/img->width
  var img-width-a/eax: (addr int) <- get img, width
  var img-width/ebx: int <- copy *img-width-a
  var img-width-f/xmm0: float <- convert img-width
  var xratio/xmm2: float <- convert width
  xratio <- divide img-width-f
  # esi = img->data
  var img-data-ah/eax: (addr handle array byte) <- get img, data
  var _img-data/eax: (addr array byte) <- lookup *img-data-ah
  var img-data/esi: (addr array byte) <- copy _img-data
  var len/edi: int <- length img-data
  #
  var one/eax: int <- copy 1
  var one-f/xmm3: float <- convert one
  var width-f/xmm4: float <- convert width
  var height-f/xmm5: float <- convert height
  var zero/eax: int <- copy 0
  var zero-f/xmm0: float <- convert zero
  var y/xmm6: float <- copy zero-f
  {
    compare y, height-f
    break-if-float>=
    var imgy-f/xmm5: float <- copy y
    imgy-f <- divide yratio
    var imgy/edx: int <- truncate imgy-f
    var x/xmm7: float <- copy zero-f
    {
      compare x, width-f
      break-if-float>=
      var imgx-f/xmm5: float <- copy x
      imgx-f <- divide xratio
      var imgx/ecx: int <- truncate imgx-f
      var idx/eax: int <- copy imgy
      idx <- multiply img-width
      idx <- add imgx
      # . multiply by 3 for the r/g/b channels
      {
        var tmp/ecx: int <- copy idx
        tmp <- shift-left 1
        idx <- add tmp
      }
      # error info in case we rounded wrong and 'index' will fail bounds-check
      compare idx, len
      {
        break-if-<
        set-cursor-position 0/screen, 0x20/x 0x20/y
        draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, imgx, 3/fg 0/bg
        draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, imgy, 4/fg 0/bg
        draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, idx, 5/fg 0/bg
      }
      # r channel
      var r: int
      {
        var src-a/eax: (addr byte) <- index img-data, idx
        var src/eax: byte <- copy-byte *src-a
        copy-to r, src
      }
      idx <- increment
      # g channel
      var g: int
      {
        var src-a/eax: (addr byte) <- index img-data, idx
        var src/eax: byte <- copy-byte *src-a
        copy-to g, src
      }
      idx <- increment
      # b channel
      var b: int
      {
        var src-a/eax: (addr byte) <- index img-data, idx
        var src/eax: byte <- copy-byte *src-a
        copy-to b, src
      }
      idx <- increment
      # plot nearest color
      var color/eax: int <- nearest-color-euclidean r, g, b
      var screenx/ecx: int <- convert x
      screenx <- add xmin
      var screeny/edx: int <- convert y
      screeny <- add ymin
      pixel screen, screenx, screeny, color
      x <- add one-f
      loop
    }
    y <- add one-f
    loop
  }
}

fn dither-ppm-unordered _src: (addr image), _dest: (addr image) {
  var src/esi: (addr image) <- copy _src
  var dest/edi: (addr image) <- copy _dest
  # copy 'width'
  var src-width-a/eax: (addr int) <- get src, width
  var tmp/eax: int <- copy *src-width-a
  var src-width: int
  copy-to src-width, tmp
  {
    var dest-width-a/edx: (addr int) <- get dest, width
    copy-to *dest-width-a, tmp
  }
  # copy 'height'
  var src-height-a/eax: (addr int) <- get src, height
  var tmp/eax: int <- copy *src-height-a
  var src-height: int
  copy-to src-height, tmp
  {
    var dest-height-a/ecx: (addr int) <- get dest, height
    copy-to *dest-height-a, tmp
  }
  # compute scaling factor 255/max
  var target-scale/eax: int <- copy 0xff
  var scale-f/xmm7: float <- convert target-scale
  var src-max-a/eax: (addr int) <- get src, max
  var tmp-f/xmm0: float <- convert *src-max-a
  scale-f <- divide tmp-f
  # allocate 'data'
  var capacity/ebx: int <- copy src-width
  capacity <- multiply src-height
  var dest/edi: (addr image) <- copy _dest
  var dest-data-ah/eax: (addr handle array byte) <- get dest, data
  populate dest-data-ah, capacity
  var _dest-data/eax: (addr array byte) <- lookup *dest-data-ah
  var dest-data/edi: (addr array byte) <- copy _dest-data
  # error buffers per r/g/b channel
  var red-errors-storage: (array int 0xc0000)
  var tmp/eax: (addr array int) <- address red-errors-storage
  var red-errors: (addr array int)
  copy-to red-errors, tmp
  var green-errors-storage: (array int 0xc0000)
  var tmp/eax: (addr array int) <- address green-errors-storage
  var green-errors: (addr array int)
  copy-to green-errors, tmp
  var blue-errors-storage: (array int 0xc0000)
  var tmp/eax: (addr array int) <- address blue-errors-storage
  var blue-errors: (addr array int)
  copy-to blue-errors, tmp
  # transform 'data'
  var src-data-ah/eax: (addr handle array byte) <- get src, data
  var _src-data/eax: (addr array byte) <- lookup *src-data-ah
  var src-data/esi: (addr array byte) <- copy _src-data
  var y/edx: int <- copy 0
  {
    compare y, src-height
    break-if->=
    var x/ecx: int <- copy 0
    {
      compare x, src-width
      break-if->=
      # - update errors and compute color levels for current pixel in each channel
      # update red-error with current image pixel
      var red-error: int
      {
        var tmp/esi: int <- _read-dithering-error red-errors, x, y, src-width
        copy-to red-error, tmp
      }
      {
        var tmp/eax: int <- _ppm-error src-data, x, y, src-width, 0/red, scale-f
        add-to red-error, tmp
      }
      # recompute red channel for current pixel
      var red-level: int
      {
        var tmp/eax: int <- _error-to-ppm-channel red-error
        copy-to red-level, tmp
      }
      # update green-error with current image pixel
      var green-error: int
      {
        var tmp/esi: int <- _read-dithering-error green-errors, x, y, src-width
        copy-to green-error, tmp
      }
      {
        var tmp/eax: int <- _ppm-error src-data, x, y, src-width, 1/green, scale-f
        add-to green-error, tmp
      }
      # recompute green channel for current pixel
      var green-level: int
      {
        var tmp/eax: int <- _error-to-ppm-channel green-error
        copy-to green-level, tmp
      }
      # update blue-error with current image pixel
      var blue-error: int
      {
        var tmp/esi: int <- _read-dithering-error blue-errors, x, y, src-width
        copy-to blue-error, tmp
      }
      {
        var tmp/eax: int <- _ppm-error src-data, x, y, src-width, 2/blue, scale-f
        add-to blue-error, tmp
      }
      # recompute blue channel for current pixel
      var blue-level: int
      {
        var tmp/eax: int <- _error-to-ppm-channel blue-error
        copy-to blue-level, tmp
      }
      # - figure out the nearest color
      var nearest-color-index/eax: int <- nearest-color-euclidean red-level, green-level, blue-level
      {
        var nearest-color-index-byte/eax: byte <- copy-byte nearest-color-index
        _write-raw-buffer dest-data, x, y, src-width, nearest-color-index-byte
      }
      # - diffuse errors
      var red-level: int
      var green-level: int
      var blue-level: int
      {
        var tmp-red-level/ecx: int <- copy 0
        var tmp-green-level/edx: int <- copy 0
        var tmp-blue-level/ebx: int <- copy 0
        tmp-red-level, tmp-green-level, tmp-blue-level <- color-rgb nearest-color-index
        copy-to red-level, tmp-red-level
        copy-to green-level, tmp-green-level
        copy-to blue-level, tmp-blue-level
      }
      # update red-error
      var red-level-error/eax: int <- copy red-level
      red-level-error <- shift-left 0x10
      subtract-from red-error, red-level-error
      _diffuse-dithering-error-floyd-steinberg red-errors, x, y, src-width, src-height, red-error
      # update green-error
      var green-level-error/eax: int <- copy green-level
      green-level-error <- shift-left 0x10
      subtract-from green-error, green-level-error
      _diffuse-dithering-error-floyd-steinberg green-errors, x, y, src-width, src-height, green-error
      # update blue-error
      var blue-level-error/eax: int <- copy blue-level
      blue-level-error <- shift-left 0x10
      subtract-from blue-error, blue-level-error
      _diffuse-dithering-error-floyd-steinberg blue-errors, x, y, src-width, src-height, blue-error
      #
      x <- increment
      loop
    }
    y <- increment
    loop
  }
}

# convert a single channel for a single image pixel to error space
fn _ppm-error buf: (addr array byte), x: int, y: int, width: int, channel: int, _scale-f: float -> _/eax: int {
  # current image pixel
  var initial-level/eax: byte <- _read-ppm-buffer buf, x, y, width, channel
  # scale to 255 levels
  var initial-level-int/eax: int <- copy initial-level
  var initial-level-f/xmm0: float <- convert initial-level-int
  var scale-f/xmm1: float <- copy _scale-f
  initial-level-f <- multiply scale-f
  initial-level-int <- convert initial-level-f
  # switch to fixed-point with 16 bits of precision
  initial-level-int <- shift-left 0x10
  return initial-level-int
}

fn _error-to-ppm-channel error: int -> _/eax: int {
  # clamp(error >> 16)
  var result/esi: int <- copy error
  result <- shift-right-signed 0x10
  {
    compare result, 0
    break-if->=
    result <- copy 0
  }
  {
    compare result, 0xff
    break-if-<=
    result <- copy 0xff
  }
  return result
}

# read from a buffer containing alternating bytes from r/g/b channels
fn _read-ppm-buffer _buf: (addr array byte), x: int, y: int, width: int, channel: int -> _/eax: byte {
  var buf/esi: (addr array byte) <- copy _buf
  var idx/ecx: int <- copy y
  idx <- multiply width
  idx <- add x
  var byte-idx/edx: int <- copy 3
  byte-idx <- multiply idx
  byte-idx <- add channel
  var result-a/eax: (addr byte) <- index buf, byte-idx
  var result/eax: byte <- copy-byte *result-a
  return result
}

# each byte in the image data is a color of the current palette
fn render-raw-image screen: (addr screen), _img: (addr image), xmin: int, ymin: int, width: int, height: int {
  var img/esi: (addr image) <- copy _img
  # yratio = height/img->height
  var img-height-a/eax: (addr int) <- get img, height
  var img-height/xmm0: float <- convert *img-height-a
  var yratio/xmm1: float <- convert height
  yratio <- divide img-height
  # xratio = width/img->width
  var img-width-a/eax: (addr int) <- get img, width
  var img-width/ebx: int <- copy *img-width-a
  var img-width-f/xmm0: float <- convert img-width
  var xratio/xmm2: float <- convert width
  xratio <- divide img-width-f
  # esi = img->data
  var img-data-ah/eax: (addr handle array byte) <- get img, data
  var _img-data/eax: (addr array byte) <- lookup *img-data-ah
  var img-data/esi: (addr array byte) <- copy _img-data
  var len/edi: int <- length img-data
  #
  var one/eax: int <- copy 1
  var one-f/xmm3: float <- convert one
  var width-f/xmm4: float <- convert width
  var height-f/xmm5: float <- convert height
  var zero/eax: int <- copy 0
  var zero-f/xmm0: float <- convert zero
  var y/xmm6: float <- copy zero-f
  {
    compare y, height-f
    break-if-float>=
    var imgy-f/xmm5: float <- copy y
    imgy-f <- divide yratio
    var imgy/edx: int <- truncate imgy-f
    var x/xmm7: float <- copy zero-f
    {
      compare x, width-f
      break-if-float>=
      var imgx-f/xmm5: float <- copy x
      imgx-f <- divide xratio
      var imgx/ecx: int <- truncate imgx-f
      var idx/eax: int <- copy imgy
      idx <- multiply img-width
      idx <- add imgx
      # error info in case we rounded wrong and 'index' will fail bounds-check
      compare idx, len
      {
        break-if-<
        set-cursor-position 0/screen, 0x20/x 0x20/y
        draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, imgx, 3/fg 0/bg
        draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, imgy, 4/fg 0/bg
        draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, idx, 5/fg 0/bg
      }
      var color-a/eax: (addr byte) <- index img-data, idx
      var color/eax: byte <- copy-byte *color-a
      var color-int/eax: int <- copy color
      var screenx/ecx: int <- convert x
      screenx <- add xmin
      var screeny/edx: int <- convert y
      screeny <- add ymin
      pixel screen, screenx, screeny, color-int
      x <- add one-f
      loop
    }
    y <- add one-f
    loop
  }
}

fn scale-image-height _img: (addr image), width: int -> _/ebx: int {
  var img/esi: (addr image) <- copy _img
  var img-height/eax: (addr int) <- get img, height
  var result-f/xmm0: float <- convert *img-height
  var img-width/eax: (addr int) <- get img, width
  var img-width-f/xmm1: float <- convert *img-width
  result-f <- divide img-width-f
  var width-f/xmm1: float <- convert width
  result-f <- multiply width-f
  var result/ebx: int <- convert result-f
  return result
}

fn next-word-skipping-comments line: (addr stream byte), out: (addr slice) {
  next-word line, out
  var retry?/eax: boolean <- slice-starts-with? out, "#"
  compare retry?, 0/false
  loop-if-!=
}