Skip to content

Commit

Permalink
Merge pull request #48 from suzukiplan/1.8.0
Browse files Browse the repository at this point in the history
Version 1.8.0
  • Loading branch information
suzukiplan authored Oct 23, 2022
2 parents 4ea5859 + 1a43eaa commit 4533434
Show file tree
Hide file tree
Showing 15 changed files with 2,866 additions and 3,004 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
- checkout
- run:
name: "Install Prerequests"
command: "sudo apt-get install -y clang clang-format g++-10"
command: "sudo apt-get install -y clang g++-10"
- run:
name: "Build and Test Z80 Emulator"
command: "make ci"
Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# Change log

## Version 1.8.0 (Oct 23, 2022 JST)

- Make strict the registers conditions at callback time (NOTE: **Destructive** change specification)
- see the details: https://github.com/suzukiplan/z80/issues/49
- When a runtime error occurs, such as executing an instruction that not exists, a crash used to occur, but now it throws a `std::runtime_error` exception.
- add new FP methods:
- `addBreakPointFP`
- `addBreakOperandFP`
- `addReturnHandlerFP`
- `addCallHandlerFP`
- add new setCallback methods:
- `setupMemoryCallback` (split from setupCallback)
- `setupDeviceCallback` (split from setupCallback)
- `setupMemoryCallbackFP` (split from setupCallbackFP)
- `setupDeviceCallbackFP` (split from setupCallbackFP)
- Implicit call the callback setter methods (`with FP`), when a function pointer that is not template is explicitly specified when a callback setter methods (`without FP`) was called.

## Version 1.7.1 (Oct 18, 2022, JST)

- A bug that prevented `addBreakOperand` from working as expected has been addressed.
Expand Down
111 changes: 96 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ void outPort(void* arg, unsigned short port, unsigned char value)

Note that by default, only the lower 8 bits of the port number can be obtained in the callback argument, and the upper 8 bits must be referenced from register B.

If you want to get it in 16 bits from the beginning, please initialize with 6th argument to `true` as follows:
If you want to get it in 16 bits from the beginning, please initialize with `returnPortAs16Bits` (6th argument) to `true` as follows:

```c++
Z80 z80(&mmu, readByte, writeByte, inPort, outPort, true);
Expand All @@ -122,23 +122,24 @@ Normally, `std::function` is used for callbacks, but in more performance-sensiti
```c++
Z80 z80(&mmu);
z80.setupCallbackFP([](void* arg, unsigned short addr) {
return 0x00; // read procedure
}, [](void* arg, unsigned char addr, unsigned char value) {
// write procedure
}, [](void* arg, unsigned short port) {
return 0x00; // input port procedure
}, [](void* arg, unsigned short port, unsigned char value) {
// output port procedure
});
z80.setupCallbackFP(readByte, writeByte, inPort, outPort);
```

However, using function pointers causes inconveniences such as the inability to specify a capture in a lambda expression.
In most cases, optimization with the `-O2` option will not cause performance problems, but in case environments where more severe performance is required, `setupCallbackFP` is recommended.

The following article (in Japanese) provides a performance comparison between function pointers and `std::function`:

https://qiita.com/suzukiplan/items/e459bf47f6c659acc74d
> The following article (in Japanese) provides a performance comparison between function pointers and `std::function`:
>
> https://qiita.com/suzukiplan/items/e459bf47f6c659acc74d
>
> The above article gives an example of the time it took to execute 100 million times call of function-pointer and three patterns of `std::function` calls (`bind`, `direct`, `lambda`) as following:
>
> |Optimization Option|function pointer|`bind`|`direct`|`lambda`|
> |:-:|-:|-:|-:|-:|
> |none|228,894μs|6,459,751μs|2,958,097μs|2,782,300μs|
> |-O|191,496μs|2,482,582μs|960868μs|668010μs|
> |-O2|100,359μs|287,467μs|313,414μs|222,633μs|
> |-Ofast|91,904μs|27,9341μs|285,927μs|188,635μs|
### 4. Execute

Expand All @@ -147,6 +148,82 @@ https://qiita.com/suzukiplan/items/e459bf47f6c659acc74d
int actualExecuteClocks = z80.execute(1234);
```

#### 4-1. Actual execute clocks

- The `execute` method repeats the execution of an instruction until the total number of clocks from the time of the call is greater than or equal to the value specified in the "clocks" argument.
- If a value less than or equal to 0 is specified, no instruction is executed at all.
- If you want single operand execution, you can specify 1.

#### 4-2. Interruption of execution

Execution of the `requestBreak` method can abort the `execute` at an arbitrary time.

> A typical 8-bit game console emulator implementation that I envision:
>
> - Implement synchronization with Video Display Processor (VDP) and other devices (sound modules, etc.) in `consumeClock` callback.
> - Call `requestBreak` when V-SYNC signal is received from VDP.
> - Call `execute` with a large value such as `INT_MAX`.
#### 4-3. Example

Code: [test/test-execute.cpp](test/test-execute.cpp)

```c++
#include "z80.hpp"

int main()
{
unsigned char rom[256] = {
0x01, 0x34, 0x12, // LD BC, $1234
0x3E, 0x01, // LD A, $01
0xED, 0x79, // OUT (C), A
0xED, 0x78, // IN A, (C)
0xc3, 0x09, 0x00, // JMP $0009
};
Z80 z80([=](void* arg, unsigned short addr) { return rom[addr & 0xFF]; },
[](void* arg, unsigned short addr, unsigned char value) {},
[](void* arg, unsigned short port) { return 0x00; },
[](void* arg, unsigned short port, unsigned char value) {
// request break the execute function after output port operand has executed.
((Z80*)arg)->requestBreak();
}, &z80);
z80.setDebugMessage([](void* arg, const char* msg) { puts(msg); });
z80.setConsumeClockCallback([](void* arg, int clocks) { printf("consume %dHz\n", clocks); });
puts("===== execute(0) =====");
printf("actualExecuteClocks = %dHz\n", z80.execute(0));
puts("===== execute(1) =====");
printf("actualExecuteClocks = %dHz\n", z80.execute(1));
puts("===== execute(0x7FFFFFFF) =====");
printf("actualExecuteClocks = %dHz\n", z80.execute(0x7FFFFFFF));
return 0;
}
```

Result is following:

```
===== execute(0) =====
actualExecuteClocks = 0Hz ... 0 is specified, no instruction is executed at all
===== execute(1) =====
consume 2Hz
consume 2Hz
consume 3Hz
consume 3Hz
[0000] LD BC<$0000>, $1234
actualExecuteClocks = 10Hz ... specify 1 to single step execution
===== execute(0x7FFFFFFF) =====
consume 2Hz
consume 2Hz
consume 3Hz
[0003] LD A<$FF>, $01
consume 2Hz
consume 2Hz
consume 4Hz
[0005] OUT (C<$34>), A<$01>
consume 4Hz
actualExecuteClocks = 19Hz ... 2147483647Hz+ is not executed but interrupted after completion of OUT due to requestBreak during OUT execution
```

### 5. Generate interrupt

#### IRQ; Interrupt Request
Expand Down Expand Up @@ -193,8 +270,9 @@ If you want to execute processing just before executing an instruction of specif
});
```

- `addBreakPoint` can set multiple breakpoints.
- `addBreakPoint` can set multiple breakpoints for the same address.
- call `removeBreakPoint` or `removeAllBreakPoints` if you want to remove the break point(s).
- call `addBreakPointFP` if you want to use the function pointer.

### Use break operand

Expand Down Expand Up @@ -224,8 +302,9 @@ If you want to execute processing just before executing an instruction of specif
```
- the opcode and length at break are stored in `opcode` and `opcodeLength` when the callback is made.
- `addBreakOperand` can set multiple breakpoints.
- `addBreakOperand` can set multiple breakpoints for the same operand.
- call `removeBreakOperand` or `removeAllBreakOperands` if you want to remove the break operand(s).
- call `addBreakOperandFP` if you want to use the function pointer.
### Detect clock consuming
Expand Down Expand Up @@ -277,6 +356,7 @@ CallHandler will be called back immediately **after** a branch by a CALL instruc
- `addCallHandler` can set multiple CallHandlers.
- call `removeAllCallHandlers` if you want to remove the CallHandler(s).
- call `addCallHandlerFP` if you want to use the function pointer.
- CallHandler also catches branches caused by interrupts.
- In the case of a condition-specified branch instruction, only the case where the branch is executed is callbacked.
Expand All @@ -301,6 +381,7 @@ ReturnHandler will be called back immediately **before** a branch by a RET instr

- `addReturnHandler` can set multiple ReturnHandlers.
- call `removeAllReturnHandlers` if you want to remove the ReturnHandler(s).
- call `addReturnHandlerFP` if you want to use the function pointer.
- In the case of a condition-specified branch instruction, only the case where the branch is executed is callbacked.

## License
Expand Down
11 changes: 7 additions & 4 deletions test-ex/Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
CFLAGS=-I../ \
-std=c++11 \
COMMON_FLAGS=-I../ \
-O2 \
-Wall \
-Wfloat-equal \
Expand All @@ -15,7 +14,7 @@ clean:
-rm cpm

cpm: cpm.cpp ../z80.hpp
clang $(CFLAGS) cpm.cpp -lstdc++ -o cpm
clang -std=c++17 $(COMMON_FLAGS) cpm.cpp -lstdc++ -o cpm

zexdoc: cpm
./cpm -e zexdoc.cim
Expand All @@ -24,8 +23,12 @@ zexall: cpm
./cpm -e zexall.cim

ci:
g++-10 $(CFLAGS) -Wclass-memaccess cpm.cpp -lstdc++ -o cpm
@echo Test zexall with gcc
g++-10 -std=c++2a $(COMMON_FLAGS) -Wclass-memaccess cpm.cpp -lstdc++ -o cpm
./cpm -e -n zexall.cim
@echo Test zexdoc with clang
clang -std=c++17 $(COMMON_FLAGS) cpm.cpp -lstdc++ -o cpm
./cpm -e -n zexdoc.cim

full: cpm
./cpm zexall.cim
Expand Down
31 changes: 17 additions & 14 deletions test-ex/cpm.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "../z80.hpp"
#include <chrono>

// CP/M Emulator (minimum implementation)
class CPM {
Expand All @@ -7,7 +8,6 @@ class CPM {
unsigned char linePointer = 0;
unsigned char memory[0x10000];
bool halted;
bool error;
bool checkError;
void (*lineCallback)(CPM*, char*);

Expand Down Expand Up @@ -35,7 +35,6 @@ class CPM {
fclose(fp);
initBios();
halted = false;
error = false;
checkError = false;
clearLineBuffer();
lineCallback = NULL;
Expand Down Expand Up @@ -126,38 +125,42 @@ int main(int argc, char* argv[])
}
cpm.checkError = checkError;
z80.reg.PC = 0x0100;
z80.addBreakOperand(0x76, [](void* arg, unsigned char* opcode, int opcodeLength) {
z80.addBreakOperandFP(0x76, [](void* arg, unsigned char* opcode, int opcodeLength) {
((CPM*)arg)->halted = true;
});
z80.addBreakPoint(0xFF04, [](void* arg) {
z80.addBreakPointFP(0xFF04, [](void* arg) {
((CPM*)arg)->halted = true;
});
if (verboseMode) {
z80.setDebugMessage([](void* arg, const char* msg) {
z80.setDebugMessageFP([](void* arg, const char* msg) {
puts(msg);
});
}
cpm.lineCallback = [](CPM* cpmPtr, char* line) {
if (cpmPtr->checkError && strstr(line, "ERROR")) {
cpmPtr->halted = true;
cpmPtr->error = true;
}
};
char animePattern[] = { '/', '-', '\\', '|' };
int anime = 0;
unsigned long totalClocks = 0;
long totalClocks = 0;
auto start = std::chrono::steady_clock::now();
bool error = true;
do {
totalClocks += (unsigned long)z80.execute(35795450); // 10sec in Z80A
if (cpm.error) {
printf("\rCPM detected an error at $%04X\n", z80.reg.PC);
} else if (cpm.halted) {
printf("CPM halted at $%04X (total: %luHz ... about %lu seconds in Z80A)\n", z80.reg.PC, totalClocks, totalClocks / 3579545);
totalClocks += (long)z80.execute(35795450); // 10sec in Z80A
if (cpm.halted) {
auto end = std::chrono::steady_clock::now();
error = z80.reg.PC != 0xFF04;
printf("CPM halted at $%04X (total: %ldHz ... about %ld seconds in Z80A)\n", z80.reg.PC, totalClocks, totalClocks / 3579545);
long us = (long)std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
long pw = totalClocks * 100000000 / 3579545 / us;
printf("Actual execution time: %ld.%ld seconds (x%ld.%ld times higher performance than Z80A)\n", us / 1000000, us % 1000000, pw / 100, pw % 100);
} else if (!noAnimation) {
putc(animePattern[anime++], stdout);
anime &= 3;
fflush(stdout);
putc(0x08, stdout);
}
} while (!cpm.halted && !cpm.error);
return cpm.error ? -1 : 0;
} while (!cpm.halted);
return error ? -1 : 0;
}
20 changes: 20 additions & 0 deletions test/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,30 @@ CFLAGS=-I../ \
-Wsign-conversion \
-Wclass-varargs \
-Wtype-limits \
-Wsequence-point \
-Wunsequenced \
-Werror

all:
make test-checkreg-on-callback
make test-execute
make test-clock
make test-status
make test-im2
make test-branch
make test-out
make test-remove-break
make test-unknown

test-execute:
clang $(CFLAGS) test-execute.cpp -lstdc++
./a.out > test-execute.txt
cat test-execute.txt

test-unknown:
clang $(CFLAGS) test-unknown.cpp -lstdc++
./a.out > test-unknown.txt
cat test-unknown.txt

test-clock:
clang $(CFLAGS) test-clock.cpp -lstdc++
Expand Down Expand Up @@ -44,3 +59,8 @@ test-remove-break:
clang $(CFLAGS) test-remove-break.cpp -lstdc++
./a.out > test-remove-break.txt
cat test-remove-break.txt

test-checkreg-on-callback:
clang $(CFLAGS) test-checkreg-on-callback.cpp -lstdc++
./a.out > test-checkreg-on-callback.txt
cat test-checkreg-on-callback.txt
6 changes: 3 additions & 3 deletions test/test-branch.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Executed a CALL instruction:
- Return to: $0007
2 ** [0011] LD A<$FF>, $01
Detected a RET instruction:
- Branch from: $0013
- Branch from: $0014
- Return to: $0007
1 * [0013] RET to $0007 (SP<$FFFB>)
1 * [0007] CP A<$01>, $FF
Expand All @@ -18,12 +18,12 @@ Executed a CALL instruction:
- Return to: $000C
2 ** [0014] LD A<$01>, $02
Detected a RET instruction:
- Branch from: $0016
- Branch from: $0017
- Return to: $000C
1 * [0016] RET to $000C (SP<$FFFB>)
1 * [000C] CALL NC, $0017 (SP<$FFFD>) <execute:NO>
1 * [000F] RET Z <execute:NO>
Detected a RET instruction:
- Branch from: $0010
- Branch from: $0011
- Return to: $0003
0 [0010] RET NZ to $0003 (SP<$FFFD>) <execute:YES>
Loading

0 comments on commit 4533434

Please sign in to comment.