lawless-legends/Platform/Apple/tools/jace/src/main/java/jace/core/TimedDevice.java

204 lines
6.5 KiB
Java

/*
* Copyright (C) 2024 Brendan Robert brendan.robert@gmail.com.
* *
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package jace.core;
import jace.config.ConfigurableField;
/**
* A timed device is a device which executes so many ticks in a given time interval. This is the core of the emulator
* timing mechanics.
*
* This basic implementation does not run freely and instead will skip cycles if it is running too fast
* This allows a parent timer to run at a faster rate without causing this device to do the same.
* Useful for devices which generate sound or video at a specific rate.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public abstract class TimedDevice extends Device {
// From the holy word of Sather 3:5 (Table 3.1) :-)
// This average speed averages in the "long" cycles
public static final long NTSC_1MHZ = 1020484L;
public static final long PAL_1MHZ = 1015625L;
public static final long SYNC_FREQ_HZ = 60;
public static final double NANOS_PER_SECOND = 1000000000.0;
public static final long NANOS_PER_MILLISECOND = 1000000L;
public static final long SYNC_SLOP = NANOS_PER_MILLISECOND * 10L; // 10ms slop for synchronization
public static int TEMP_SPEED_MAX_DURATION = 1000000;
@ConfigurableField(name = "Speed", description = "(Percentage)")
public int speedRatio = 100;
@ConfigurableField(name = "Max speed")
public boolean forceMaxspeed = false;
public boolean maxspeed = false;
private long cyclesPerSecond = defaultCyclesPerSecond();
private int cycleTimer = 0;
private int tempSpeedDuration = 0;
private long nanosPerInterval; // How long to wait between pauses
private long cyclesPerInterval; // How many cycles to wait until a pause interval
private long nextSync = System.nanoTime(); // When is the next sync interval supposed to finish?
protected Runnable unthrottledTick = () -> super.doTick();
Long waitUntil = null;
protected Runnable throttledTick = () -> {
if (waitUntil == null || System.nanoTime() >= waitUntil) {
super.doTick();
waitUntil = calculateResyncDelay();
}
};
protected final Runnable tickHandler;
/**
* Creates a new instance of TimedDevice, setting default speed
* Protected as overriding the tick handler should only done
* for the independent timed device
*/
protected TimedDevice(boolean throttleUsingTicks) {
super();
setSpeedInHz(defaultCyclesPerSecond());
tickHandler = throttleUsingTicks ? throttledTick : unthrottledTick;
resetSyncTimer();
}
public TimedDevice() {
this(true);
}
@Override
public final void doTick() {
tickHandler.run();
}
public final void resetSyncTimer() {
nextSync = System.nanoTime() + nanosPerInterval;
cycleTimer = 0;
}
@Override
public boolean suspend() {
disableTempMaxSpeed();
return super.suspend();
}
@Override
public void setPaused(boolean paused) {
if (!isPaused() && paused) {
pauseStart();
}
super.setPaused(paused);
if (!paused) {
resetSyncTimer();
}
}
protected void pauseStart() {
// Override if you need to add a pause behavior
}
@Override
public void resume() {
super.resume();
setPaused(false);
resetSyncTimer();
}
public final int getSpeedRatio() {
return speedRatio;
}
public final void setMaxSpeed(boolean enabled) {
maxspeed = enabled;
if (!enabled) {
resetSyncTimer();
}
}
public final boolean isMaxSpeedEnabled() {
return maxspeed;
}
public final boolean isMaxSpeed() {
return forceMaxspeed || maxspeed;
}
public final long getSpeedInHz() {
return cyclesPerSecond;
}
public final void setSpeedInHz(long newSpeed) {
// System.out.println("Raw set speed for " + getName() + " to " + cyclesPerSecond + "hz");
// Thread.dumpStack();
cyclesPerSecond = newSpeed;
speedRatio = (int) Math.round(cyclesPerSecond * 100.0 / defaultCyclesPerSecond());
cyclesPerInterval = cyclesPerSecond / SYNC_FREQ_HZ;
nanosPerInterval = (long) (cyclesPerInterval * NANOS_PER_SECOND / cyclesPerSecond);
// System.out.println("Will pause " + nanosPerInterval + " nanos every " + cyclesPerInterval + " cycles");
resetSyncTimer();
}
public final void setSpeedInPercentage(int ratio) {
cyclesPerSecond = defaultCyclesPerSecond() * ratio / 100;
if (cyclesPerSecond == 0) {
cyclesPerSecond = defaultCyclesPerSecond();
}
setSpeedInHz(cyclesPerSecond);
}
public void enableTempMaxSpeed() {
tempSpeedDuration = TEMP_SPEED_MAX_DURATION;
}
public void disableTempMaxSpeed() {
tempSpeedDuration = 0;
resetSyncTimer();
}
@Override
public void reconfigure() {
resetSyncTimer();
}
public long defaultCyclesPerSecond() {
return NTSC_1MHZ;
}
private boolean useParentTiming() {
if (getParent() != null && getParent() instanceof TimedDevice) {
TimedDevice pd = (TimedDevice) getParent();
if (pd.useParentTiming() || (!pd.isMaxSpeed() && pd.getSpeedInHz() <= getSpeedInHz())) {
return true;
}
}
return false;
}
protected Long calculateResyncDelay() {
if (++cycleTimer < cyclesPerInterval) {
return null;
}
cycleTimer = 0;
long retVal = nextSync;
nextSync = Math.max(nextSync, System.nanoTime()) + nanosPerInterval;
if (isMaxSpeed() || useParentTiming()) {
if (tempSpeedDuration > 0) {
tempSpeedDuration -= cyclesPerInterval;
if (tempSpeedDuration <= 0) {
disableTempMaxSpeed();
}
}
return null;
}
return retVal;
}
}