QB64-Phoenix-Edition / QB64pe

The QB64 Phoenix Edition Repository
https://qb64phoenix.com
Other
132 stars 26 forks source link

Optionally add underclocking of TIMER and potentially overall. #495

Open Poikilos opened 6 months ago

Poikilos commented 6 months ago

Is your feature request related to a problem? Please describe. Back when QBasic was popular for making indie games, a "tick" wasn't a fixed millisecond or microsecond. It could be a CPU cycle, and entirely dependent on the clock speed of the CPU. Certain old QBasic games, like certain other DOS games, depend on the speed of the CPU in order to run properly. For example, if a game programmed for a 66 MHz processor were played on a 133Mhz processor, the game would be approximately double the speed and be unplayable. Specifically, TIMER seems to be the CPU cycle divided by 18.2 (according to https://www.tek-tips.com/faqs.cfm?fid=917), which seems like entirely different behavior than described at https://qb64phoenix.com/qb64wiki/index.php/TIMER. There were some games that tried to count the number of TIMER ticks in one second, which I tried to do, but once the clock speed got above a certain point, that wouldn't work anymore and would just max out the CPU then cause an integer overflow...unless my math didn't account for it flipping around to 0 more quickly than expected. Even if it did, my code isn't the only code out there. Games that expected TIMER not to flip to 0 (or not to flip to 0 more than once) over the course of 1 frame still wouldn't work. In the worse case, some games don't even attempt to use frame locking--they just assume the computer's speed, and only work correctly on old computers (games of this era typically only run at the correct speed on a 33MHz or 66MHz processor).

Describe the solution you'd like A "virtual MHz" compile option to affect either or both (maybe allow enabling each separately):

  1. TIMER
  2. processing speed in general (Maybe FLOPS would need to be calculated from user-set MHz?? See tankgame example below)

Describe alternatives you've considered DOSBox or changing the code: but neither is ideal since compatibility with old code is a goal of this project.

Additional context List of framerate locking methods of old QBasic games (may require being split into different issues):

mkilgore commented 6 months ago

An exact CPU cycle match is not a goal of this project, it really isn't possible due to the way QB64 works. As such a setting to emulate a specific MHz CPU speed just isn't going to be implemented (mostly because it's not really possible to do in an effective way). That said, the other things are a bit different:

  1. I think TIMER should already work for this, though I haven't tried it. QBasic help defines the timer as "the number of seconds since midnight", but as your page points out the TIMER counting is limited by the speed of the timer interrupt in the old hardware. QB64 matches the same TIMER resolution by default so I think this should work (though note what I say about this at the end).
  2. Reprogramming the timer interrupt to be faster or slower is not supported, but an issue could be made for that. I think it's unlikely we would add it though, there's only small bits of the hardware that we emulate for backwards compatibility. The timer interrupt is messy because it probably impacts a lot of stuff beyond just TIMER and in weird ways that would be hard to match (and we wouldn't necessarily want to match them anyway).
  3. The WAIT examples are interesting. Currently QB64 does not offer a way to wait for a screen refresh, it's an outstanding issue here. That said, once that issue is fixed it sounds like it may be possible to make those WAIT commands work as well. It would not be directly tied to the screen refresh but QB64's screen redraw (which is always 60 FPS at the moment)
  4. To me the SOUND thing just seems like a bug, but I think @a740g should chime in on that one over on #494.

I would say overall though that delays like looping on TIMER are bad in QB64 because they cause 100% CPU usage while you're looping. New commands like _Delay and _Limit can be used to replace these delays and bring down CPU usage. Those do require code changes to add (we can't detect where they should go) but they're hopefully minimal for your programs.

Poikilos commented 6 months ago

Thanks for providing the alternatives and input on other issues. I suppose (with the suggested optional mode) accessing TIMER could actually sleep CPU when the TIMER global is accessed (let CPU rest 1ms maybe? The math would have to be worked out, but that may be fine enough since even locking to 60fps is 16.6... [repeating 6] ms so it is probably fine-grained enough) so then you'd know where the delay should go (:edit: except for games that just assume a clock speed and don't even do frame locking. That's why I mentioned FLOPS...it would be possible to count the number of FLOPS and limit the FLOPS that way, placing the delay wherever math is done too many times per second, though maybe that is ugly). My goal is to get old programs to work. Certain games have awkward custom licenses not allowing redistribution, but I understand you don't want to encourage bad patterns. I'll try to find a timer example used in this way (which would max CPU if code is used literally) but so far I only found:

Just demos, so it is hard to tell if speed is as expected:

grymmjack commented 6 months ago

@Poikilos thanks so much for sharing this repo: https://github.com/robhagemans/hoard-of-gwbasic/tree/master !

Poikilos commented 6 months ago

@grymmjack You're welcome. I found it almost by chance! GitHub (using Linguist) often doesn't label repos as BASIC correctly, but:

grymmjack commented 6 months ago

@Poikilos Oh that is a great trick! I will add my own language detection fixes to my repos.

Smart thinking on how to search too! You might find some more by using path:*.bi and/or path:*.bm :)