added data stack, breakpoints, and character coordinates :)
This commit is contained in:
parent
5b9312f643
commit
10559bde8b
File diff suppressed because it is too large
Load Diff
|
@ -8,4 +8,5 @@ edition = "2021"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bdf = "0.6.0"
|
bdf = "0.6.0"
|
||||||
bitvec = "1.0.1"
|
bitvec = "1.0.1"
|
||||||
|
iced = { version = "0.12.0", features = ["canvas", "smol"] }
|
||||||
minifb = "0.25.0"
|
minifb = "0.25.0"
|
||||||
|
|
34
src/cpu.rs
34
src/cpu.rs
|
@ -1,7 +1,8 @@
|
||||||
use crate::error::{ExecutionError, GeorgeError, GeorgeErrorKind, MemoryError};
|
use crate::error::{ExecutionError, GeorgeError, GeorgeErrorKind, MemoryError};
|
||||||
use crate::instructions::{get_instruction, Instruction};
|
use crate::instructions::{get_instruction, Instruction};
|
||||||
use crate::memory::Mem;
|
use crate::memory::{self, Mem};
|
||||||
use crate::types::{Byte, Word};
|
use crate::types::{Byte, Word};
|
||||||
|
use std::process::exit;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::{
|
use std::{
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
|
@ -166,7 +167,7 @@ impl Cpu {
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
pub fn cycle(&mut self) -> Result<(), GeorgeError> {
|
pub fn cycle(&mut self) -> Result<(), GeorgeError> {
|
||||||
sleep(Duration::from_nanos(500));
|
sleep(Duration::from_nanos(100));
|
||||||
if self.pending_cycles == 0 {
|
if self.pending_cycles == 0 {
|
||||||
if self.nmi || (self.irq && !self.get_flag(StatusFlag::IrqDisable)) {
|
if self.nmi || (self.irq && !self.get_flag(StatusFlag::IrqDisable)) {
|
||||||
let _ = self.push_stack_word(self.pc);
|
let _ = self.push_stack_word(self.pc);
|
||||||
|
@ -228,15 +229,32 @@ impl Cpu {
|
||||||
self.pc, valid_instruction).to_string(), kind: error})
|
self.pc, valid_instruction).to_string(), kind: error})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Instruction::Invalid(invalid_instruction) => {
|
Instruction::Invalid(invalid_instruction) => match invalid_instruction.opcode {
|
||||||
return Err(GeorgeError {
|
0x02 => {
|
||||||
kind: GeorgeErrorKind::Execution(ExecutionError::InvalidInstruction),
|
let memory = match self.memory.try_read() {
|
||||||
desc: format!(
|
Ok(read) => read,
|
||||||
|
Err(_) => {
|
||||||
|
println!("Couldn't acquire read lock on memory in cpu thread");
|
||||||
|
return Err(GeorgeError {
|
||||||
|
kind: GeorgeErrorKind::Memory(MemoryError::Unwritable),
|
||||||
|
desc: "Couldn't acquire read lock on memory in cpu thread"
|
||||||
|
.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
memory.dump().unwrap();
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(GeorgeError {
|
||||||
|
kind: GeorgeErrorKind::Execution(ExecutionError::InvalidInstruction),
|
||||||
|
desc: format!(
|
||||||
"An invalid instruction with opcode {:#04x} was called at address {:#06x}",
|
"An invalid instruction with opcode {:#04x} was called at address {:#06x}",
|
||||||
invalid_instruction.opcode, self.pc
|
invalid_instruction.opcode, self.pc
|
||||||
),
|
),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.pending_cycles -= 1;
|
self.pending_cycles -= 1;
|
||||||
|
|
BIN
src/george
BIN
src/george
Binary file not shown.
221
src/george.asm
221
src/george.asm
|
@ -1,25 +1,204 @@
|
||||||
.setcpu "65C02"
|
.setcpu "65C02"
|
||||||
.segment "CODE"
|
.segment "CODE"
|
||||||
LDA #$60
|
|
||||||
STA $01
|
; okay so rn i wanna set up a very basic system init, and write a few subroutines to draw characters at x,y coordinates
|
||||||
LDY #$0
|
n = $01 ; temporary storage for data stack operations
|
||||||
fill:
|
|
||||||
LDA #$20
|
.macro breakpoint ; $02 isn't a valid instruction, the emulator will see this and halt, dump memory contents
|
||||||
STY $00
|
.byte $02
|
||||||
STA ($00)
|
.endmacro
|
||||||
INY
|
|
||||||
CPY #$ff
|
.macro pop ; drops a stack cell
|
||||||
BNE fill
|
inx
|
||||||
|
inx
|
||||||
|
.endmacro
|
||||||
|
|
||||||
|
.macro pop2 ; drops 2 data stack cells
|
||||||
|
inx
|
||||||
|
inx
|
||||||
|
inx
|
||||||
|
inx
|
||||||
|
.endmacro
|
||||||
|
|
||||||
|
.macro push ;
|
||||||
|
dex
|
||||||
|
dex
|
||||||
|
.endmacro
|
||||||
|
|
||||||
|
.macro push2 ;
|
||||||
|
dex
|
||||||
|
dex
|
||||||
|
dex
|
||||||
|
dex
|
||||||
|
.endmacro
|
||||||
|
|
||||||
|
.macro to_r ; pop the top of the stack off and save it in the return (hardware) stack: (n -- )
|
||||||
|
lda 1, x
|
||||||
|
pha
|
||||||
|
lda 0, x
|
||||||
|
pha
|
||||||
|
pop
|
||||||
|
.endmacro
|
||||||
|
|
||||||
|
.macro from_r ; pop the top of the return stack off and put it on the data stack: ( -- n)
|
||||||
|
push
|
||||||
|
pla
|
||||||
|
sta 0, x
|
||||||
|
pla
|
||||||
|
sta 1, x
|
||||||
|
.endmacro
|
||||||
|
|
||||||
|
init:
|
||||||
|
ldx #0; initialize data stack pointer
|
||||||
|
jsr initdisplay
|
||||||
|
|
||||||
main:
|
main:
|
||||||
LDY #$0
|
jsr draw
|
||||||
STY $6000
|
|
||||||
LDY #$1
|
initdisplay:
|
||||||
STY $6001
|
lda #$20
|
||||||
LDY #$2
|
ldy #0
|
||||||
STY $6002
|
jsr cleardisplay
|
||||||
LDY #$1
|
rts
|
||||||
STY $6003
|
|
||||||
LDY #$3
|
cleardisplay:
|
||||||
STY $6004
|
sta $6000,y
|
||||||
JMP main
|
sta $6100,y
|
||||||
|
sta $6200,y
|
||||||
|
sta $6300,y
|
||||||
|
sta $6400,y
|
||||||
|
sta $6500,y
|
||||||
|
sta $6600,y
|
||||||
|
sta $6700,y ; this goes slightly over but it's fine
|
||||||
|
iny
|
||||||
|
bne cleardisplay
|
||||||
|
rts
|
||||||
|
|
||||||
|
; TODO: get this to work, (also i think emu-side i need better tooling), rn it just draws a heart to $6000, so i think the addition isn't working or something
|
||||||
|
draw: ; draw a character at (20, 30)
|
||||||
|
lda #63
|
||||||
|
push
|
||||||
|
sta 0, x ; low byte
|
||||||
|
stz 1,x ; high byte is zero
|
||||||
|
lda #28
|
||||||
|
push
|
||||||
|
sta 0,x ; same here
|
||||||
|
stz 1,x
|
||||||
|
jsr get_char_address ; calculate where to put the character in memory
|
||||||
|
lda #4
|
||||||
|
sta (0, x) ; store a at the address pointed to on the stack
|
||||||
|
brk
|
||||||
|
|
||||||
|
get_char_address: ; gets vram address for a character at (x, y),
|
||||||
|
; (n1: x n2: y -- n: $6000 + x + (64 * y))
|
||||||
|
;jsr push_lit ; push 64 onto stack, low byte first
|
||||||
|
;.byte 64
|
||||||
|
;.byte 0
|
||||||
|
lda #64
|
||||||
|
push ; doing this instead until `push_lit` is fixed
|
||||||
|
sta 0, x
|
||||||
|
stz 1, x
|
||||||
|
jsr mult ; multiply 64 with y (n2)
|
||||||
|
jsr plus ; add result with x (n1)
|
||||||
|
|
||||||
|
;jsr push_lit ; push vram address onto the stack
|
||||||
|
;.byte $00
|
||||||
|
;.byte $60
|
||||||
|
lda #$60
|
||||||
|
push
|
||||||
|
sta 1, x
|
||||||
|
stz 0, x
|
||||||
|
jsr plus ; add vram start address to result
|
||||||
|
|
||||||
|
rts
|
||||||
|
|
||||||
|
; inc16
|
||||||
|
; add 1 to a 16-bit pointer in zero page
|
||||||
|
|
||||||
|
;inc16:
|
||||||
|
; inc ptr
|
||||||
|
; bne :+
|
||||||
|
; inc ptr+1
|
||||||
|
;: rts
|
||||||
|
;
|
||||||
|
|
||||||
|
; on this channel we love garth wilson: https://wilsonminesco.com/stacks/StackOps.ASM
|
||||||
|
; data stack is built up of 2-byte cells
|
||||||
|
|
||||||
|
|
||||||
|
; TODO: this is broken, the return address gets mangled somewhere in here, could be an emulator problem tho
|
||||||
|
push_lit: ; this bad boy lets you inline a literal (low byte first) right after `jsr push_lit` and put it on the stack, once again, on this channel we love garth wilson
|
||||||
|
push2
|
||||||
|
phx
|
||||||
|
tsx
|
||||||
|
txa
|
||||||
|
tay
|
||||||
|
plx
|
||||||
|
|
||||||
|
lda $102, y
|
||||||
|
sta 0, x
|
||||||
|
clc
|
||||||
|
adc #2
|
||||||
|
sta $102, y
|
||||||
|
|
||||||
|
lda $103, y
|
||||||
|
sta 1, x
|
||||||
|
adc #0
|
||||||
|
sta $103, y
|
||||||
|
|
||||||
|
fetch:
|
||||||
|
lda (0, x)
|
||||||
|
pha
|
||||||
|
inc 0, x
|
||||||
|
bne @1
|
||||||
|
inc 1, x
|
||||||
|
@1: lda (0, x)
|
||||||
|
bra put
|
||||||
|
push
|
||||||
|
|
||||||
|
put:
|
||||||
|
sta 1, x
|
||||||
|
pla
|
||||||
|
sta 0, x
|
||||||
|
rts
|
||||||
|
|
||||||
|
plus: ; add: (n1 n2 -- n1+n2)
|
||||||
|
clc
|
||||||
|
lda 0, x
|
||||||
|
adc 2, x
|
||||||
|
sta 2, x
|
||||||
|
lda 1, x
|
||||||
|
adc 3, x
|
||||||
|
sta 3, x
|
||||||
|
pop
|
||||||
|
rts
|
||||||
|
|
||||||
|
|
||||||
|
mult: ; multiply: (n1 n2 -- n1*n2), frankly, i don't know how this works, but TODO: will try to figure it out later
|
||||||
|
phy
|
||||||
|
stz n
|
||||||
|
ldy #0
|
||||||
|
@1: lsr 3, x
|
||||||
|
ror 2, x
|
||||||
|
bcc @2
|
||||||
|
clc
|
||||||
|
lda n
|
||||||
|
adc 0, x
|
||||||
|
sta n
|
||||||
|
tya
|
||||||
|
adc 1, x
|
||||||
|
tay
|
||||||
|
@2: asl 0, x
|
||||||
|
rol 1, x
|
||||||
|
lda 2, x
|
||||||
|
ora 3, x
|
||||||
|
bne @1
|
||||||
|
lda n
|
||||||
|
sta 2, x
|
||||||
|
sty 3, x
|
||||||
|
pop
|
||||||
|
ply
|
||||||
|
rts
|
||||||
|
|
||||||
|
|
||||||
|
.segment "VRAM"
|
||||||
|
|
|
@ -8,4 +8,5 @@ MEMORY {
|
||||||
|
|
||||||
SEGMENTS {
|
SEGMENTS {
|
||||||
CODE: load = "PROGRAM", type = rw;
|
CODE: load = "PROGRAM", type = rw;
|
||||||
|
VRAM: load = "VRAM", type = rw;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1900,7 +1900,7 @@ fn get_address(
|
||||||
) -> Result<AddressingModeValue, ExecutionError> {
|
) -> Result<AddressingModeValue, ExecutionError> {
|
||||||
let byte = cpu.read(cpu.pc)?;
|
let byte = cpu.read(cpu.pc)?;
|
||||||
cpu.pc += 1;
|
cpu.pc += 1;
|
||||||
let address: Word = (byte + cpu.x) as Word;
|
let address: Word = (byte.wrapping_add(cpu.x)) as Word;
|
||||||
Ok(AddressingModeValue::Absolute(address))
|
Ok(AddressingModeValue::Absolute(address))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2072,6 +2072,7 @@ impl Opcode {
|
||||||
// check out https://docs.rs/emulator_6502/latest/src/emulator_6502/opcodes/mod.rs.html
|
// check out https://docs.rs/emulator_6502/latest/src/emulator_6502/opcodes/mod.rs.html
|
||||||
);
|
);
|
||||||
cpu.set_flag(StatusFlag::Negative, cpu.is_negative(result as Byte));
|
cpu.set_flag(StatusFlag::Negative, cpu.is_negative(result as Byte));
|
||||||
|
cpu.a = result as Byte;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
_ => Err(GeorgeErrorKind::AddrMode(
|
_ => Err(GeorgeErrorKind::AddrMode(
|
||||||
|
@ -2607,7 +2608,7 @@ impl Opcode {
|
||||||
let byte = cpu.read(address.try_into()?)?;
|
let byte = cpu.read(address.try_into()?)?;
|
||||||
cpu.set_flag(StatusFlag::Carry, cpu.a >= byte);
|
cpu.set_flag(StatusFlag::Carry, cpu.a >= byte);
|
||||||
cpu.set_flag(StatusFlag::Zero, cpu.a == byte);
|
cpu.set_flag(StatusFlag::Zero, cpu.a == byte);
|
||||||
cpu.set_flag(StatusFlag::Negative, cpu.is_negative(cpu.a - byte));
|
cpu.set_flag(StatusFlag::Negative, cpu.a <= byte);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
_ => Err(GeorgeErrorKind::AddrMode(
|
_ => Err(GeorgeErrorKind::AddrMode(
|
||||||
|
@ -2660,7 +2661,7 @@ impl Opcode {
|
||||||
},
|
},
|
||||||
Opcode::DEX(mode) => match mode {
|
Opcode::DEX(mode) => match mode {
|
||||||
AddressingMode::Implied => {
|
AddressingMode::Implied => {
|
||||||
cpu.x -= 1;
|
cpu.x = cpu.x.wrapping_sub(1);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
_ => Err(GeorgeErrorKind::AddrMode(
|
_ => Err(GeorgeErrorKind::AddrMode(
|
||||||
|
@ -2698,7 +2699,7 @@ impl Opcode {
|
||||||
},
|
},
|
||||||
Opcode::INC(mode) => match mode {
|
Opcode::INC(mode) => match mode {
|
||||||
AddressingMode::Accumulator => {
|
AddressingMode::Accumulator => {
|
||||||
cpu.a += 1;
|
cpu.a = cpu.a.wrapping_add(1);
|
||||||
cpu.set_flag(StatusFlag::Zero, cpu.a == 0);
|
cpu.set_flag(StatusFlag::Zero, cpu.a == 0);
|
||||||
cpu.set_flag(StatusFlag::Negative, cpu.is_negative(cpu.a));
|
cpu.set_flag(StatusFlag::Negative, cpu.is_negative(cpu.a));
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -2720,7 +2721,9 @@ impl Opcode {
|
||||||
},
|
},
|
||||||
Opcode::INX(mode) => match mode {
|
Opcode::INX(mode) => match mode {
|
||||||
AddressingMode::Implied => {
|
AddressingMode::Implied => {
|
||||||
cpu.x += 1;
|
cpu.x = cpu.x.wrapping_add(1);
|
||||||
|
cpu.set_flag(StatusFlag::Zero, cpu.x == 0);
|
||||||
|
cpu.set_flag(StatusFlag::Negative, cpu.is_negative(cpu.x));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
_ => Err(GeorgeErrorKind::AddrMode(
|
_ => Err(GeorgeErrorKind::AddrMode(
|
||||||
|
@ -2729,7 +2732,9 @@ impl Opcode {
|
||||||
},
|
},
|
||||||
Opcode::INY(mode) => match mode {
|
Opcode::INY(mode) => match mode {
|
||||||
AddressingMode::Implied => {
|
AddressingMode::Implied => {
|
||||||
cpu.y += 1;
|
cpu.y = cpu.y.wrapping_add(1);
|
||||||
|
cpu.set_flag(StatusFlag::Zero, cpu.y == 0);
|
||||||
|
cpu.set_flag(StatusFlag::Negative, cpu.is_negative(cpu.y));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
_ => Err(GeorgeErrorKind::AddrMode(
|
_ => Err(GeorgeErrorKind::AddrMode(
|
||||||
|
@ -3113,9 +3118,9 @@ impl Opcode {
|
||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
Opcode::RTS(mode) => match mode {
|
Opcode::RTS(mode) => match mode {
|
||||||
AddressingMode::Implied => {
|
AddressingMode::Stack => {
|
||||||
let return_address = cpu.pop_stack_word()?;
|
let return_address = cpu.pop_stack_word()?;
|
||||||
cpu.pc = return_address + 1;
|
cpu.pc = return_address + 3; // Go back to where we jsr'ed, skipping the operand
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
_ => Err(GeorgeErrorKind::AddrMode(
|
_ => Err(GeorgeErrorKind::AddrMode(
|
||||||
|
|
|
@ -56,4 +56,5 @@ fn main() {
|
||||||
cpu.execute();
|
cpu.execute();
|
||||||
});
|
});
|
||||||
screen.run();
|
screen.run();
|
||||||
|
shared_memory.write().unwrap().dump().unwrap();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
use crate::error::{GeorgeError, GeorgeErrorKind, MappingError, MemoryError};
|
use crate::error::{GeorgeError, GeorgeErrorKind, MappingError, MemoryError};
|
||||||
use crate::types::{Byte, Word};
|
use crate::types::{Byte, Word};
|
||||||
|
use std::io::{self, Write};
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::{fs::File, io::Read};
|
use std::{fs::File, io::Read};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -59,6 +61,18 @@ impl Mem {
|
||||||
pub fn new(area: MemMappedDevice) -> Self {
|
pub fn new(area: MemMappedDevice) -> Self {
|
||||||
Self { areas: vec![area] }
|
Self { areas: vec![area] }
|
||||||
}
|
}
|
||||||
|
pub fn dump(&self) -> io::Result<()> {
|
||||||
|
let mut outfile = File::create("./coredump.bin")?;
|
||||||
|
let mut data = Vec::new();
|
||||||
|
for area in &self.areas {
|
||||||
|
for byte in &area.data {
|
||||||
|
data.push(byte.to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outfile.set_len(0xFFFF)?;
|
||||||
|
outfile.write_all(&data)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
pub fn add_area(&mut self, area: MemMappedDevice) -> Result<(), GeorgeError> {
|
pub fn add_area(&mut self, area: MemMappedDevice) -> Result<(), GeorgeError> {
|
||||||
for existing_area in &self.areas {
|
for existing_area in &self.areas {
|
||||||
if existing_area.contains(area.end) || existing_area.contains(area.start) {
|
if existing_area.contains(area.end) || existing_area.contains(area.start) {
|
||||||
|
|
31
src/video.rs
31
src/video.rs
|
@ -1,5 +1,5 @@
|
||||||
use crate::Mem;
|
use crate::Mem;
|
||||||
use minifb::{Window, WindowOptions};
|
use minifb::{Key, Scale, ScaleMode, Window, WindowOptions};
|
||||||
use std::{
|
use std::{
|
||||||
fs::File,
|
fs::File,
|
||||||
io::Read,
|
io::Read,
|
||||||
|
@ -27,7 +27,22 @@ pub fn get_char_bin(path: &str) -> Vec<u8> {
|
||||||
|
|
||||||
impl Crtc {
|
impl Crtc {
|
||||||
pub fn new(memory: Arc<RwLock<Mem>>) -> Self {
|
pub fn new(memory: Arc<RwLock<Mem>>) -> Self {
|
||||||
let window = Window::new("screen", 512, 380, WindowOptions::default()).unwrap();
|
let window = Window::new(
|
||||||
|
"ʕ·ᴥ·ʔ-☆",
|
||||||
|
512,
|
||||||
|
380,
|
||||||
|
WindowOptions {
|
||||||
|
resize: true,
|
||||||
|
borderless: true,
|
||||||
|
title: true,
|
||||||
|
transparency: false,
|
||||||
|
scale: Scale::X2,
|
||||||
|
scale_mode: ScaleMode::AspectRatioStretch,
|
||||||
|
topmost: false,
|
||||||
|
none: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
let char_rom = get_char_bin("./src/cozette.bin");
|
let char_rom = get_char_bin("./src/cozette.bin");
|
||||||
Self {
|
Self {
|
||||||
memory,
|
memory,
|
||||||
|
@ -64,11 +79,14 @@ impl Crtc {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// the rest of this function is arcane wizardry based on the specifics of george's weird
|
||||||
|
// display and characters... don't fuck around w it
|
||||||
|
let mut i = 0;
|
||||||
for char_row in 0..29 {
|
for char_row in 0..29 {
|
||||||
for char_col in 0..64 {
|
for char_col in 0..64 {
|
||||||
let ascii = memory
|
let ascii = memory.read(0x6000 + i).unwrap();
|
||||||
.read(0x6000 + (char_row as u16 * char_col as u16))
|
i += 1;
|
||||||
.unwrap();
|
|
||||||
for row in 0..13 {
|
for row in 0..13 {
|
||||||
let byte = self.char_rom[ascii as usize + (row * 0x101)];
|
let byte = self.char_rom[ascii as usize + (row * 0x101)];
|
||||||
for i in (0..8).rev() {
|
for i in (0..8).rev() {
|
||||||
|
@ -108,6 +126,9 @@ impl Crtc {
|
||||||
let mut previous_draw = Instant::now();
|
let mut previous_draw = Instant::now();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
if self.window.is_key_down(Key::Q) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
if now - previous_draw > frame_duration {
|
if now - previous_draw > frame_duration {
|
||||||
self.draw();
|
self.draw();
|
||||||
|
|
Loading…
Reference in New Issue