summaryrefslogtreecommitdiff
path: root/emulator.sql
blob: 10bb18aac3a47b49752ffe31c2ad4f1b0e978e5e (plain)
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
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
drop schema if exists private cascade;

create schema private;

create table private.constants
(
    id         integer primary key,
    label      varchar(20) not null,
    value      integer,
    value_word integer
);

create index idx_constants_label on private.constants (label);

create table private.registers
(
    id    integer primary key,
    value integer not null
);

create table private.state
(
    id         integer primary key,
    label      varchar(20) not null,
    value      integer,
    value_word integer
);

create table private.timers
(
    id    integer primary key,
    label varchar(20) not null,
    value integer     not null
);

create table private.inputs
(
    id    integer primary key,
    value integer not null
);

create table private.memory
(
    address integer primary key,
    value   integer not null
);

create table private.display
(
    i     integer primary key,
    value boolean
);

create table private.stack
(
    id    integer primary key,
    value integer not null
);

-- contains the current opcode and its parts for easy access
-- parts are: opcode, k, x, y, n, nn, nnn
create or replace view private.opcode_parts as
select s.value_word               as opcode,
       s.value_word & 61440 >> 12 as k,
       (s.value_word & 3840) >> 8 as x,
       (s.value_word & 240) >> 4  as y,
       s.value_word & 15          as n,
       s.value_word & 255         as nn,
       s.value_word & 4095        as nnn
from private.state s
where s.id = 4;

create or replace function private.reset()
    returns void as
$$
begin
    truncate table private.constants;
    insert into private.constants (id, label, value, value_word)
    values (1, 'Font Start', null, 61440),
           (2, 'Program Start', null, 512),
           (3, 'Screen Width', 64, null),
           (4, 'Screen Height', 32, null),
           (5, 'Register Count', 16, null),
           (6, 'Stack Depth', 16, null),
           (7, 'Input Count', 16, null);


    truncate table private.state;
    insert into private.state (id, label, value, value_word)
    values (1, 'Program Counter', null, (select value_word from private.constants where label = 'Program Start')),
           (2, 'Index Register', null, 0),
           (3, 'Stack Pointer', 0, null),
           (4, 'Opcode', null, 0),
           (5, 'Screen Dirty', 0, null);


    truncate table private.registers;
    insert into private.registers (id, value)
    select gs, 0
    from generate_series(0,
                         (select value from private.constants where label = 'Register Count') - 1) as gs;


    truncate table private.memory;
    insert into private.memory (address, value)
    select gs, 0
    from generate_series(0, 4095) as gs;


    truncate table private.display;
    insert into private.display (i, value)
    select gs, false
    from generate_series(0,
                         (select value from private.constants where label = 'Screen Width') *
                         (select value from private.constants where label = 'Screen Height') - 1) as gs;


    truncate table private.timers;
    insert into private.timers (id, label, value)
    values (1, 'Delay Timer', 0),
           (2, 'Sound Timer', 0);


    truncate table private.stack;
    insert into private.stack (id, value)
    select gs, 0
    from generate_series(0,
                         (select value from private.constants where label = 'Stack Depth') - 1) as gs;


    truncate table private.inputs;
    insert into private.inputs (id, value)
    select gs, 0
    from generate_series(0,
                         (select value from private.constants where label = 'Input Count') - 1) as gs;
end;
$$ language plpgsql;


create or replace function private.read_register(register_id integer)
    returns integer as
$$
declare
    reg_value integer;
begin
    select value
    into reg_value
    from private.registers
    where id = register_id;
    return reg_value;
end;
$$ language plpgsql;

create or replace function private.write_register(register_id integer, new_value integer)
    returns void as
$$
begin
    update private.registers
    set value = new_value
    where id = register_id;
end;
$$ language plpgsql;

create or replace function private.push_stack(new_value integer)
    returns void as
$$
begin
    -- stack[SP] = new_value
    update private.stack
    set value = new_value
    where id = (select value from private.state where id = 3);

    -- SP++
    update private.state
    set value = value + 1
    where id = 3;
end;
$$ language plpgsql;

create or replace function private.pop_stack()
    returns integer as
$$
declare
    stack_value integer;
begin
    -- SP--
    update private.state
    set value = value - 1
    where id = 3;

    -- return stack[SP]
    select value
    into stack_value
    from private.stack
    where id = (select value from private.state where id = 3);
    return stack_value;
end;
$$ language plpgsql;

create or replace function private.read_memory(read_address integer)
    returns integer as
$$
declare
    mem_value integer;
begin
    select value
    into mem_value
    from private.memory
    where address = read_address;
    return mem_value;
end;
$$ language plpgsql;

create or replace function private.write_memory(write_address integer, new_value integer)
    returns void as
$$
begin
    update private.memory
    set value = new_value
    where address = write_address;
end;
$$ language plpgsql;

create or replace function private.fetch()
    returns void as
$$
begin
    -- fetch the opcode at the current program counter
    update private.state
    set value_word =
            ((select value from private.memory where address = (select value_word from private.state where id = 1)) << 8)
                |
            (select value from private.memory where address = (select value_word from private.state where id = 1) + 1)
    where id = 4;
end;
$$ language plpgsql;

create or replace function private.increment_program_counter()
    returns void as
$$
begin
    update private.state
    set value_word = value_word + 2
    where id = 1;
end;
$$ language plpgsql;

-- section for opcode execution functions

-- JUMP
create or replace function private.exec_1nnn(op private.opcode_parts)
    returns void as
$$
begin
    update private.state
    set value_word = op.nnn
    where id = 1; -- Program Counter
end;
$$ language plpgsql;

-- CALL
create or replace function private.exec_2nnn(op private.opcode_parts)
    returns void as
$$
declare
    current_pc integer;
begin
    select value_word
    into current_pc
    from private.state
    where id = 1;

    perform private.push_stack(current_pc);

    update private.state
    set value_word = op.nnn
    where id = 1;
end;
$$ language plpgsql;


-- RETURN
create or replace function private.exec_group_0(op private.opcode_parts)
    returns void as
$$
begin
    if op.opcode = 238 then -- 0x00EE = 238 decimal
        update private.state
        set value_word = private.pop_stack()
        where id = 1;
    elsif op.opcode = 224 then -- 0x00E0 = 224 decimal
        update private.display
        set value = false;
    else
        raise exception 'Unknown 0-group opcode %', op.opcode;
    end if;
end;
$$ language plpgsql;

-- SET REGISTER
create or replace function private.exec_6xnn(op private.opcode_parts)
    returns void as
$$
begin
    update private.registers
    set value = op.nn
    where id = op.x;
end;
$$ language plpgsql;

-- ADD TO REGISTER
create or replace function private.exec_7xnn(op private.opcode_parts)
    returns void as
$$
begin
    update private.registers
    set value = (value + op.nn) % 256
    where id = op.x;
end;
$$ language plpgsql;

-- SKIP IF EQUAL
create or replace function private.exec_3xnn(op private.opcode_parts)
    returns void as
$$
declare
    vx integer;
begin
    select value
    into vx
    from private.registers
    where id = op.x;

    if vx = op.nn then
        update private.state
        set value_word = value_word + 2
        where id = 1;
    end if;
end;
$$ language plpgsql;

-- SKIP IF NOT EQUAL
create or replace function private.exec_4xnn(op private.opcode_parts)
    returns void as
$$
declare
    vx integer;
begin
    select value
    into vx
    from private.registers
    where id = op.x;

    if vx != op.nn then
        update private.state
        set value_word = value_word + 2
        where id = 1;
    end if;
end;
$$ language plpgsql;

-- SKIP IF REGISTERS EQUAL
create or replace function private.exec_5xy0(op private.opcode_parts)
    returns void as
$$
declare
    vx integer;
    vy integer;
begin
    select value into vx from private.registers where id = op.x;
    select value into vy from private.registers where id = op.y;

    if vx = vy then
        update private.state
        set value_word = value_word + 2
        where id = 1;
    end if;
end;
$$ language plpgsql;

-- SKIP IF REGISTERS NOT EQUAL
create or replace function private.exec_9xy0(op record)
    returns void as
$$
declare
    vx integer;
    vy integer;
begin
    select value into vx from private.registers where id = op.x;
    select value into vy from private.registers where id = op.y;

    if vx <> vy then
        update private.state set value_word = value_word + 2 where id = 1;
    end if;
end;
$$ language plpgsql;

-- ARITHMETIC AND BITWISE OPERATIONS
create or replace function private.exec_group_8(op record)
    returns void as
$$
declare
    vx     integer;
    vy     integer;
    result integer;
begin
    select value into vx from private.registers where id = op.x;
    select value into vy from private.registers where id = op.y;

    case op.n
        when 0 then update private.registers set value = vy where id = op.x;
        when 1 then update private.registers set value = (vx | vy) where id = op.x;
        when 2 then update private.registers set value = (vx & vy) where id = op.x;
        when 3 then update private.registers set value = (vx # vy) where id = op.x;
        when 4 then
            result := vx + vy;
            update private.registers set value = result % 256 where id = op.x;
            update private.registers set value = (result > 255)::int where id = 15;
        when 5 then
            update private.registers set value = (vx - vy + 256) % 256 where id = op.x;
            update private.registers set value = (vx >= vy)::int where id = 15;
        when 6 then
            update private.registers set value = vx >> 1 where id = op.x;
            update private.registers set value = (vx & 1) where id = 15;
        when 7 then
            update private.registers set value = (vy - vx + 256) % 256 where id = op.x;
            update private.registers set value = (vy >= vx)::int where id = 15;
        when 14 then
            update private.registers set value = (vx << 1) % 256 where id = op.x;
            update private.registers set value = ((vx & 128) >> 7) where id = 15;
        else raise exception 'Unknown 8xy? opcode %', op.opcode;
        end case;
end;
$$ language plpgsql;

-- SET INDEX REGISTER
create or replace function private.exec_annn(op record)
    returns void as
$$
begin
    update private.state set value_word = op.nnn where id = 2;
end;
$$ language plpgsql;

-- JUMP WITH OFFSET
create or replace function private.exec_bnnn(op record)
    returns void as
$$
declare
    v0 integer;
begin
    select value into v0 from private.registers where id = 0;

    update private.state
    set value_word = op.nnn + v0
    where id = 1;
end;
$$ language plpgsql;

-- RANDOM NUMBER
create or replace function private.exec_cxnn(op record)
    returns void as
$$
declare
    rnd integer;
begin
    rnd := floor(random() * 256);
    update private.registers
    set value = (rnd & op.nn)
    where id = op.x;
end;
$$ language plpgsql;

-- INPUT
create or replace function private.exec_group_e(op record)
    returns void as
$$
declare
    vx      integer;
    pressed integer;
begin
    select value into vx from private.registers where id = op.x;
    select value into pressed from private.inputs where id = vx;

    if op.nn = 158 then -- 9E
        if pressed = 1 then
            update private.state set value_word = value_word + 2 where id = 1;
        end if;

    elsif op.nn = 161 then -- A1
        if pressed = 0 then
            update private.state set value_word = value_word + 2 where id = 1;
        end if;

    else
        raise exception 'Unknown Ex?? opcode %', op.opcode;
    end if;
end;
$$ language plpgsql;

-- FGROUP
create or replace function private.exec_group_f(op record)
    returns void as
$$
declare
    vx integer;
    i  integer;
begin
    select value into vx from private.registers where id = op.x;
    select value_word into i from private.state where id = 2;

    case op.nn

        when 7 then -- Fx07
        update private.registers
        set value = (select value from private.timers where label = 'Delay Timer')
        where id = op.x;

        when 15 then -- Fx15
        update private.timers set value = vx where label = 'Delay Timer';

        when 18 then -- Fx18
        update private.timers set value = vx where label = 'Sound Timer';

        when 30 then -- Fx1E
        update private.state set value_word = i + vx where id = 2;

        when 41 then -- Fx29
        update private.state set value_word = vx * 5 where id = 2;

        when 51 then -- Fx33 (BCD)
        update private.memory set value = vx / 100 where address = i;
        update private.memory set value = (vx / 10) % 10 where address = i + 1;
        update private.memory set value = vx % 10 where address = i + 2;

        when 85 then -- Fx55
        for i in 0..op.x
            loop
                update private.memory
                set value = (select value from private.registers where id = i)
                where address = i + i;
            end loop;

        when 101 then -- Fx65
        for i in 0..op.x
            loop
                update private.registers
                set value = (select value from private.memory where address = i + i)
                where id = i;
            end loop;

        else raise exception 'Unknown Fx?? opcode %', op.opcode;
        end case;
end;
$$ language plpgsql;

create or replace function private.exec_dxyn(op record)
    returns void as
$$
declare
    vx          integer;
    vy          integer;
    i_reg           integer;
    sprite_row  integer;
    sprite_byte integer;
    x_pos       integer;
    y_pos       integer;
    pixel_idx   integer;
    old_pixel   boolean;
    collision   integer := 0;
    row         integer;
    col         integer;
begin
    -- fetch registers and I
    select value into vx from private.registers where id = op.x;
    select value into vy from private.registers where id = op.y;
    select value_word into i_reg from private.state where id = 2;

    -- clear VF
    update private.registers set value = 0 where id = 15;

    -- for each row of sprite
    for row in 0..op.n - 1
        loop
            -- read byte from memory
            select value into sprite_byte from private.memory where address = i_reg + row;

            -- draw each bit
            for col in 0..7
                loop
                    -- only process if bit is 1
                    if ((sprite_byte >> (7 - col)) & 1) = 1 then
                        -- calculate pixel coordinates (wrap)
                        x_pos := (vx + col) % 64;
                        y_pos := (vy + row) % 32;

                        pixel_idx := y_pos * 64 + x_pos;

                        -- get old pixel
                        select value into old_pixel from private.display where private.display.i = pixel_idx;

                        -- check collision
                        if old_pixel then
                            collision := 1;
                        end if;

                        -- XOR pixel
                        update private.display
                        set value = not old_pixel
                        where private.display.i = pixel_idx;
                    end if;
                end loop;
        end loop;

    -- set VF if any collision
    if collision = 1 then
        update private.registers set value = 1 where id = 15;
    end if;

    -- mark screen dirty
    update private.state
    set value = 1
    where id = 5;
end;
$$ language plpgsql;

-- exit section

create or replace function private.mark_screen_clean()
    returns void as
$$
begin
    update private.state
    set value = 0
    where id = 5;
end;
$$ language plpgsql;

create or replace function private.is_screen_dirty()
    returns boolean as
$$
declare
    dirty boolean;
begin
    select value::boolean
    into dirty
    from private.state
    where id = 5;
    return dirty;
end;
$$ language plpgsql;

create or replace function private.clock()
    returns void as
$$
declare
    op private.opcode_parts;
begin
    -- fetch
    perform private.fetch();
    perform private.increment_program_counter();

    -- opcode dispatch
    select *
    into op
    from private.opcode_parts;

    case op.k
        when 0 then perform private.exec_group_0(op);
        when 1 then perform private.exec_1nnn(op);
        when 2 then perform private.exec_2nnn(op);
        when 3 then perform private.exec_3xnn(op);
        when 4 then perform private.exec_4xnn(op);
        when 5 then perform private.exec_5xy0(op);
        when 6 then perform private.exec_6xnn(op);
        when 7 then perform private.exec_7xnn(op);
        when 8 then perform private.exec_group_8(op);
        when 9 then perform private.exec_9xy0(op);
        when 10 then perform private.exec_annn(op);
        when 11 then perform private.exec_bnnn(op);
        when 12 then perform private.exec_cxnn(op);
        when 13 then perform private.exec_dxyn(op);
        when 14 then perform private.exec_group_e(op);
        when 15 then perform private.exec_group_f(op);
        else raise exception 'Unknown opcode %', op.opcode;
        end case;

    -- decrement timers if they are above 0
    update private.timers
    set value = value - 1
    where value > 0;
end;
$$ language plpgsql;


select private.reset();

--select private.clock();