-
-
Notifications
You must be signed in to change notification settings - Fork 54
Implement consistent PC value in callbacks #12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
fix consistent PC value in callbacks
|
I just noticed that your current code attempts to fix this issue—for I did not test this PR under I believe this PR change of properly fixing the However, there may be further complications that I do not understand, and I do not have the ability to fully test the necessary further code modification of removing your I'm sure you realized all this right away as soon as you saw this PR, but I just thought I'd suggest that perhaps getting rid of |
|
Temporarily retracted. |
|
There is a strong reason to increment PC before calling callbacks that receive a memory address as an argument, which depends on PC, for example, when reading instruction bytes. The reason is that when using the value of PC to calculate the memory address argument passed to the callback, we are already performing a memory access to In other words, if we increment PC before calling the callbacks, we will only need to read its value once, whereas if we increment it afterward, we will need to read it twice. Regarding However, there is an optimization that could be applied in certain cases, for example, in Regarding the purpose of this change: You should know that our intention has never been to guarantee the coherence of registers during the execution of an instruction. In other words, the correctness of the PC value and other registers is not ensured until the instruction has finished executing. As a result, we do not guarantee that a callback can access either the initial or final value of a register. Keep in mind that this emulator is used in projects involving very slow microcontrollers with very limited resources, and we have always prioritized speed when optimizing it. That said, if what you need is to know when an instruction starts, you might want to take a look at my Z80InsnClock library. It could be useful to you. |
|
Thank you for the PR. As Sofía has explained, PC is incremented before being used to compute the memory address passed to the callback for speed reasons. In any case, perhaps it would be a good idea, as far as possible, to always increment PC at the beginning of the function, and not at the end as is done in some places, whenever the function calls a callback or another non-inline function to eliminate the need for the caller to restore its argument on some architectures. |
This would only be necessary in some cases. For example, it would not be here, since it is already necessary to restore INSN(ld_a_vbc) {Q_0 MEMPTR = BC + 1; A = READ(BC); PC++; return 7;}On the other hand, I don't think it is worthwhile to make callbacks always able to access the final value of PC, because it would complicate block instructions like this one: #define LDXR(operator) \
zuint8 t = READ(HL operator); \
\
WRITE(DE operator, t); \
t += A; \
\
if (--BC) \
{ /* HF, NF = 0 */ \
FLAGS = F_SZC | /* SF, ZF, CF unchanged */ \
((PC >> 8) & YXF) | /* YF = PCi.13; XF = PCi.11 */ \
PF; /* PF = 1 */ \
\
MEMPTR = PC + 1; \
return 21; \
} \
\
FLAGS = (zuint8)( /* HF, PF, NF = 0 */ \
F_SZC | /* SF, ZF, CF unchanged */ \
((t & 2) << 4) | /* YF = (A + [HLi]).1 */ \
(t & XF)); /* XF = (A + [HLi]).3 */ \
\
PC += 2; \
return 16 |
|
I'm going to make the optimizations I said, and improve some other things a little bit. I'm going to modify quite a few lines. |
|
Thank you both for the reply. I assume that you briefly examined the code that I naively proposed. While I largely agree with your use-case, API design, and functional (domain-relevant) arguments against making the My thought here is that excellent performance could be maintained, possibly even improved, and at least any losses mitigated through careful use of a saved local The prevailing, unadjusted scalar Also, the various details and edge cases of the so-called predictable |
One question: why do you need PC to be predictable after the opcode fetch? take into account that PC points to the first byte of the instruction during the invocation of |
|
My understanding is that with multiple prefixes, it is not the case that |
|
|
You can get the T-state of the current M-cycle relative to the start of the instruction using my Z80InsnClock library, and know wether the opcode fetch corresponds to the first byte of the insn: #include <Z80.h>
#include <Z80IncnClock.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
uint8_t *memory;
Z80 cpu;
Z80InsnClock insn_clock;
} Machine;
uint8_t machine_cpu_fetch_opcode(Machine *self, uint16_t address)
{
uint8_t opcode = self->memory[address];
uint8_t insn_cycles = z80_insn_clock_m1(&self->insn_clock, opcode);
if (!insn_cycles && Z80_PC(self->cpu) == address)
{
/* this is first opcode fetch of the instruction */
}
else {
/* this is not the first opcode fetch of the instruction */
}
return opcode;
}
uint8_t machine_cpu_read(Machine *self, uint16_t address)
{
uint8_t insn_cycles = z80_insn_clock_m(&self->insn_clock);
/* ... */
return self->memory[address];
}
void machine_cpu_write(Machine *self, uint16_t address, uint8_t value)
{
uint8_t insn_cycles = z80_insn_clock_m(&self->insn_clock);
/* ... */
self->memory[address] = value;
}
uint8_t machine_cpu_in(Machine *self, uint16_t address)
{
uint8_t insn_cycles = z80_insn_clock_m(&self->insn_clock);
/* ... */
return 0xFF /* read byte from device */;
}
void machine_cpu_out(Machine *self, uint16_t address, uint8_t value)
{
uint8_t insn_cycles = z80_insn_clock_m(&self->insn_clock);
/* ... */
/* write value to device */
}
uint8_t machine_insn_clock_read(Machine *self, uint16_t address)
{
return self->memory[address];
}
void machine_initialize(Machine *self)
{
self->memory = malloc(65536);
self->cpu.context = self;
self->cpu.fetch_opcode = (Z80Read)machine_cpu_fetch_opcode;
self->cpu.nop = nullptr;
self->cpu.fetch =
self->cpu.read = (Z80Read )machine_cpu_read;
self->cpu.write = (Z80Write)machine_cpu_write;
self->cpu.in = (Z80Read )machine_cpu_in;
self->cpu.out = (Z80Write)machine_cpu_out;
self->cpu.halt = nullptr;
self->cpu.ld_i_a =
self->cpu.ld_r_a =
self->cpu.reti =
self->cpu.retn = nullptr;
self->cpu.hook = nullptr;
self->cpu.illegal = nullptr;
z80_insn_clock_initialize(
&self->insn_clock,
&self->cpu.af, &self->cpu.bc, &self->cpu.hl,
self, (Z80InsnClockRead)machine_insn_clock_read);
}
void machine_power_on(Machine *self)
{
memset(self->memory, 0, 65536);
z80_power(&self->cpu, true);
z80_insn_clock_start(&self->insn_clock, self->cpu.resume);
}
void machine_reset(Machine *self)
{
z80_reset(&self->cpu);
z80_insn_clock_start(&self->insn_clock, self->cpu.resume);
} |
|
This is exactly what I was looking for, thanks so much! |
|
@agaxia's example is great, although figuring out wether the opcode fetch corresponds to the first byte of the instruction is trivial even when you don't need to know the intra-instruction T-states. Example: uint8_t machine_cpu_fetch_opcode(Machine *self, uint16_t address)
{
uint8_t opcode = self->memory[address];
if (Z80_PC(self->cpu) == address)
{
/* this is the first opcode fetch of the instruction */
}
else {
/* this is not the first opcode fetch of the instruction */
}
return opcode;
}PC is updated only once per instruction. Therefore, INSN(cb_prefix)
{
R++;
return cb_insn_table[DATA[1] = FETCH_OPCODE((PC += 2) - 1)](self);
}but during the second opcode fetch As a final note, |
That will not work in instructions with the That's why the condition checks |
I think we are both wrong. It should be: #define IS_XY_PREFIX(opcode) (((opcode) & 0xDF) == 0xDD)
if (!insn_cycles && (Z80_PC(self->cpu) == address || IS_XY_PREFIX(opcode))
{
/* this is the first opcode fetch of the instruction */
}Your instruction clock takes into account that the Z80 emulator treats the If the previous opcode fetch was a If not using Z80InsnClock, the condition would be: if ( Z80_PC(self->cpu) == address ||
(IS_XY_PREFIX(self->cpu.data.uint8_array[0]) && IS_XY_PREFIX(opcode))
)
{
/* this is the first opcode fetch of the instruction */
} |
|
Oh... I see, I forgot that in my example 😅. Yes, you are right 👍 |
Currently, the status of the
PCregister value is inconsistent during any callback, depending on the precise details of the conditions that generate the callback.This PR changes this behavior so that the
PCvalue observed during anyFETCH,READorWRITEcallback has a consistent interpretation, which is that it always still reflects the unadvancedPCvalue from the beginning of the instruction that is enacting that callback operation.Of course the actual effective address for the operation (as passed in through the callback argument) is unaffected.
Because this PR inherently addresses the outwards visibility of a stored state value, it is obviously a breaking change relative to the prior behavior of your code. For this reason, I am offering this PR as a courtesy only, which you may feel free to ignore or adopt as you wish.
Legal notice (do not delete)
Contributors are required to assign copyright to the original author of the project so that he retains full ownership. This ensures that other entities can use the software more easily, as they only need to deal with a single copyright holder. It also provides the original author with the assurance that he can make decisions in the future without needing to consult or obtain consent from all contributors.
By submitting this pull request (PR), you agree to the following: