Even a simple submodule is holding it's secrets (Patreon)
Content
Hi everyone,
in the last post I asked you about your opinion: should I continue working on the PSX core or do something else. I read all comments and thought about it.
First of all, I want to thank everyone for the kind words. It's really great to see that you like my work so much and just trust that whatever I do for fun will be of value for you. Thank you so much!
There have also been many people who would be happy if PSX work would conitinue, so that's what I will do for now: my work on the PSX will continue until end of this year and then I will ask you again.
That's 4 more months for now where we hopefully get some more things fixed and more accuracy improved. It will be slower than the last year however: we are getting in a dark zone that hasn't been touched much before, as you will see in todays post.
What I'm currently doing is writing tests. Little programs in .exe format that can be executed on the core, emulators and executed on the real PSX. I use these tests to reverse engineer the behavior of the PSX. This knowledge can then be used to improve the core (or emulators) and regression test in the future.
Today I want to present you one example of how that works. Even with it being a very simple submodule that is tested, you will see that a lot of knowledge is required to get the information from deep inside the PSX.
What we look at is the Timer module. The Timer module can be used as a stopwatch by the games, e.g. to count up time for a racing game or sync audio to gameplay or determine how fast the action on the screen should be. So it's crucial to have this part as accurate as possible, otherwise small errors might sum up.
The Timer submodule contains 3 independent timers that count up a value. In the easiest case the value is just counted up by 1 every clock cycle of the CPU. You can reset it to zero, set it to any value and it has a range of 0 to 65535, which means that after counting up to 65535 it will automatically wrap over to zero.
This little part of the documention already has 1 mistake and 1 inaccuracy as we will see later.
So how can we now test this? It seems rather simple: we reset the value to zero and read back the value after e.g. 5 clock cycles to see if it hold the value of 5.
That's what I did: write a test in assembler and does exactly that, using different wait times between reset and read. And that's already where the trouble begins.
Here is the behavior of the old core in this case:
The name of the subtests shows how many NOPs are executed between resetting the timer and reading it's value. NOP stands for No Operation and means that the CPU just does an idle cycle there without calculating anything.
You can see that with 1 NOPs, the PS1 reads back a value of zero, with 2 NOPs it's 1 and with 9 NOPs it's 8. So it's counting up fine and the value is always 1 less than the amount of NOPS executed.
The core also counts up fine, but has an offset of 3 cycles. That's why I called this test calibration. I have written it to find out when the reset really takes place after writing it from the CPU.
You can also see that some tests are executed for different memory regions: KUSEG, KSEG1 and KSEG2. Each memory region contains a mirror of these register. This test shows that the different regions behave the same for the Timer submodule, so I can ignore the region in the subsequent tests.
And we have the first finding here: executing no NOPs or 1 NOP between reset and readout of the timer has no effect on the PS1. That means, that a reset will hold for 2 cycles. This is something that has not yet been found it seems, as the documentation tells nothing about it and every emulator I found has that wrong.
So overall we need to fix 2 things: reset for 2 cycles and execute the reset delayed by 2 cycles. Let's implement that:
We have the Timer calibrated in the core with this change, which will be crucial for the tests that follow now.
Let's test the wraparound at 65535 next. We should need to execute 65536 NOPs to read back 65535 from the timer and then it should wrap to zero. The interesting question is: will it stay at zero for 2 clock cycles like with the reset?
As you can see it does not, the value of 0 is only held for 1 clock cycle, but other than that, the assumptions have fulfilled and it works fine.
The next function to test is the target value. The timer can be configured to not wrap around at 65535, but instead at a certain target value.
We should now test how the timer reacts on reaching these target values:
When will it reset to zero? When the value is reached or later?
Will it stay at zero for 1 cycle like with the 65535 wraparound or will it stay there for 2 cycles like with the reset from CPU?
At this point I have to apologize that the tests I wrote don't look better. More text and explanation would be nice here. Those programs should be written in assembler, as we need full control on what is executed and things like nice UI is not easy to do in assembler programming.
The name of the test now tells you at which target value the timer should reset and the number behind tells again how many NOPs are executing between reset and readout.
e.g. TIMERWRAP1 2 means that the target value is 1 and we wait for 2 cycles before we read back.
We can see multiple results and findings here:
- the timer reaches the target value before it's reset to zero
- when reset to zero, it stays at zero for another clock cycle
- with a target value of zero, the Timer never counts up
Result 1+2 together also tells us about the cycle time of the timer. When for example the target is set to 10, the timer will run from 0 to 10, which is 11 cycles and it will stay at zero for another cycle => 12 cycles. So the cycle time is always the target value + 2.
Again here: no emulator I found has that right. Some even have some serious bugs like and for example count up to 65535 when the target is zero or never reach the target value, but reset 1 cycle earlier.
As the core copied the Timer behavior of DuckStation before this research, to have a solid starting point, it also shared some of this bugs until the recent fix.
One more on the list: setting the current value by the cpu instead of resetting to zero and count up from there.
With this test we want to see:
- will the set value count up instantly or will it remain at the set value for another clock cycle?
- what happens when the set value is below, at or above the target value?
- does it matter if we reset just before or after setting the current value?
So lets look through the subtests:
- 9 NOP KSEG1 is just the calibration subtest again to have it as reference here
- Current 1 and 3 is setting the Current value to 1 or 3 instead of doing a reset. You can see that it just counts up as with the reset, with the offset being as large as the current value is, no special things here
- Then we have 4 subtests that apply a reset before or after setting the current value. There is no influence at all. So we don't have to care about what happens in which order
The last part is the intresting one. We test what happens when the current value is set ABOVE the target value. The documentation clearly says that "when exceeding the selected target value"..."It gets forcefully reset to 0000h"
Is that really true?
We must do some decoding again: Subtest TARGET 7 CURRENT 9 2 means, that the target is set to 7, the current value to 9 and there is a delay of 2 clock cycles before reading back the value.
So in this subtest we set the current value above the target value and would assume it would reset like the documentastion says, but that's not the case. The timer just counts up from 9 after holding the value for 1 additional clock cycle.
Same happens if we are closer with a target value of 8.
As soon as target and current value are set equal, the timer will indeed reset to zero instead of counting up further.
Overall, we learned a lot of the details about the Timers with those tests and there is likely more to come, as the Timers have some advanced features that involve GPU timings. But for now, the core fulfills all these tests with the fixes made.
You can look at the tests and execute them yourself if you like:
https://github.com/RobertPeip/PSX
If you execute those tests on a old core version or any emulator, you will get results that seem completly random.
While it does somehow show how inaccurate the timer start, wraparound and reset really are, it's a bit unfair, because every subtest after the first calibration ones highly depends on the emulator being accurate with the basic reset point, otherwise it will of course show every other subtest as totally wrong.
Please keep that in mind when doing comparisons of the core against e.g. DuckStation or Mednafen. Those are great emulator nontheless, even with the reset point being off and some edge cases not fulfilled.
Have fun!