Skip to content

Commit

Permalink
Redo sleeping as an opcode
Browse files Browse the repository at this point in the history
- Remove `sleep` from DMStandard.
- Two new opcodes Sleep and BackgroundSleep (which is just `sleep -1`, much faster than popping a `float` off for background sleeps).
- Create new lightweight async proc state for sleeping.
- Compiler/runtime adjustments to handle new opcodes.
  • Loading branch information
Cyberboss committed Jan 1, 2024
1 parent f0e4f27 commit 86fb394
Show file tree
Hide file tree
Showing 17 changed files with 175 additions and 31 deletions.
2 changes: 2 additions & 0 deletions DMCompiler/Bytecode/DreamProcOpcode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ public enum DreamProcOpcode : byte {
Log = 0x81,
LogE = 0x82,
Abs = 0x83,
[OpcodeMetadata(stackDelta: -1)] Sleep = 0x84,
BackgroundSleep = 0x85,
}

/// <summary>
Expand Down
17 changes: 17 additions & 0 deletions DMCompiler/Compiler/DM/DMAST.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ public void VisitProcStatementSpawn(DMASTProcStatementSpawn statementSpawn) {
throw new NotImplementedException();
}

public void VisitProcStatementSleep(DMASTProcStatementSleep statementSleep) {
throw new NotImplementedException();
}

public void VisitProcStatementIf(DMASTProcStatementIf statementIf) {
throw new NotImplementedException();
}
Expand Down Expand Up @@ -920,6 +924,19 @@ public override void Visit(DMASTVisitor visitor) {
}
}

public sealed class DMASTProcStatementSleep : DMASTProcStatement {
public DMASTExpression Delay;

public DMASTProcStatementSleep(Location location, DMASTExpression delay) :
base(location) {
Delay = delay;
}

public override void Visit(DMASTVisitor visitor) {
visitor.VisitProcStatementSleep(this);
}
}

public sealed class DMASTProcStatementSpawn : DMASTProcStatement {
public DMASTExpression Delay;
public readonly DMASTProcBlockInner Body;
Expand Down
1 change: 1 addition & 0 deletions DMCompiler/Compiler/DM/DMLexer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public sealed class DMLexer : TokenLexer {
{ "call", TokenType.DM_Call },
{ "call_ext", TokenType.DM_Call},
{ "spawn", TokenType.DM_Spawn },
{ "sleep", TokenType.DM_Sleep },
{ "goto", TokenType.DM_Goto },
{ "step", TokenType.DM_Step },
{ "try", TokenType.DM_Try },
Expand Down
19 changes: 19 additions & 0 deletions DMCompiler/Compiler/DM/DMParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ public DMParser(DMLexer lexer) : base(lexer) {
TokenType.DM_Null,
TokenType.DM_Switch,
TokenType.DM_Spawn,
TokenType.DM_Sleep,
TokenType.DM_Do,
TokenType.DM_While,
TokenType.DM_For,
Expand Down Expand Up @@ -715,6 +716,7 @@ public DMASTFile File() {
procStatement ??= Switch();
procStatement ??= Continue();
procStatement ??= Break();
procStatement ??= Sleep();
procStatement ??= Spawn();
procStatement ??= While();
procStatement ??= DoWhile();
Expand Down Expand Up @@ -1059,6 +1061,23 @@ private DMASTProcStatementSet[] ProcSetEnd(bool allowMultiple) {
}
}

public DMASTProcStatementSleep? Sleep() {
var loc = Current().Location;

if (Check(TokenType.DM_Sleep)) {
Whitespace();
bool hasParenthesis = Check(TokenType.DM_LeftParenthesis);
Whitespace();
DMASTExpression? delay = Expression();
if (delay == null) Error("Expected delay to sleep for");
if (hasParenthesis) ConsumeRightParenthesis();

return new DMASTProcStatementSleep(loc, delay ?? new DMASTConstantInteger(loc, 0));
} else {
return null;
}
}

public DMASTProcStatementIf? If() {
var loc = Current().Location;

Expand Down
21 changes: 10 additions & 11 deletions DMCompiler/DM/DMProc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -459,20 +459,19 @@ public void MarkLoopContinue(string loopLabel) {
AddLabel($"{loopLabel}_continue");
}

public void BackgroundSleep() {
// TODO This seems like a bad way to handle background, doesn't it?

if ((Attributes & ProcAttributes.Background) == ProcAttributes.Background) {
if (!DMObjectTree.TryGetGlobalProc("sleep", out var sleepProc)) {
throw new CompileErrorException(Location, "Cannot do a background sleep without a sleep proc");
}

PushFloat(-1); // argument given to sleep()
Call(DMReference.CreateGlobalProc(sleepProc.Id), DMCallArgumentsType.FromStack, 1);
Pop(); // Pop the result of the sleep call
public void SleepDelayPushed() => WriteOpcode(DreamProcOpcode.Sleep);

public void Sleep(float delay) {
if(delay == -1.0f) // yielding
WriteOpcode(DreamProcOpcode.BackgroundSleep);
else {
PushFloat(delay);
WriteOpcode(DreamProcOpcode.Sleep);
}
}

public void BackgroundSleep() => WriteOpcode(DreamProcOpcode.BackgroundSleep);

public void LoopJumpToStart(string loopLabel) {
BackgroundSleep();
Jump($"{loopLabel}_start");
Expand Down
4 changes: 4 additions & 0 deletions DMCompiler/DM/Visitors/DMASTSimplifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ public void VisitProcStatementSpawn(DMASTProcStatementSpawn statementSpawn) {
statementSpawn.Body.Visit(this);
}

public void VisitProcStatementSleep(DMASTProcStatementSleep statementSleep) {
SimplifyExpression(ref statementSleep.Delay);
}

public void VisitProcStatementGoto(DMASTProcStatementGoto statementGoto) {

}
Expand Down
16 changes: 16 additions & 0 deletions DMCompiler/DM/Visitors/DMProcBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ public void ProcessStatement(DMASTProcStatement statement) {
case DMASTProcStatementBreak statementBreak: ProcessStatementBreak(statementBreak); break;
case DMASTProcStatementDel statementDel: ProcessStatementDel(statementDel); break;
case DMASTProcStatementSpawn statementSpawn: ProcessStatementSpawn(statementSpawn); break;
case DMASTProcStatementSleep statementSleep: ProcessStatementSleep(statementSleep); break;
case DMASTProcStatementReturn statementReturn: ProcessStatementReturn(statementReturn); break;
case DMASTProcStatementIf statementIf: ProcessStatementIf(statementIf); break;
case DMASTProcStatementFor statementFor: ProcessStatementFor(statementFor); break;
Expand Down Expand Up @@ -317,6 +318,21 @@ public void ProcessStatementSpawn(DMASTProcStatementSpawn statementSpawn) {
_proc.AddLabel(afterSpawnLabel);
}

public void ProcessStatementSleep(DMASTProcStatementSleep statementSleep) {
var expr = DMExpression.Create(_dmObject, _proc, statementSleep.Delay);
if (expr.TryAsConstant(out var constant)) {
if (constant is Number constantNumber) {
_proc.Sleep(constantNumber.Value);
return;
}

constant.EmitPushValue(_dmObject, _proc);
} else
expr.EmitPushValue(_dmObject, _proc);

_proc.SleepDelayPushed();
}

public void ProcessStatementVarDeclaration(DMASTProcStatementVarDeclaration varDeclaration) {
if (varDeclaration.IsGlobal) { return; } //Currently handled by DMObjectBuilder

Expand Down
1 change: 0 additions & 1 deletion DMCompiler/DMStandard/_Standard.dm
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ proc/roll(ndice = 1, sides)
proc/round(A, B)
proc/sha1(input)
proc/shutdown(Addr,Natural = 0)
proc/sleep(Delay)
proc/sorttext(T1, T2)
proc/sorttextEx(T1, T2)
proc/sound(file, repeat = 0, wait, channel, volume)
Expand Down
4 changes: 2 additions & 2 deletions OpenDreamRuntime/Procs/AsyncNativeProc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

namespace OpenDreamRuntime.Procs {
public sealed class AsyncNativeProc : DreamProc {
public sealed class State : ProcState {
public sealed class State : AsyncProcState {
public static readonly Stack<State> Pool = new();

// IoC dependencies instead of proc fields because _proc can be null
Expand Down Expand Up @@ -52,7 +52,7 @@ public void Initialize(AsyncNativeProc? proc, Func<State, Task<DreamValue>> task
}

// Used to avoid reentrant resumptions in our proc
public void SafeResume() {
public override void SafeResume() {
if (_inResume) {
return;
}
Expand Down
5 changes: 5 additions & 0 deletions OpenDreamRuntime/Procs/AsyncProcState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace OpenDreamRuntime.Procs {
public abstract class AsyncProcState : ProcState {
public abstract void SafeResume();
}
}
81 changes: 80 additions & 1 deletion OpenDreamRuntime/Procs/DMOpcodeHandlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
using OpenDreamRuntime.Resources;
using OpenDreamShared.Dream;
using Robust.Shared.Random;

using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute;
using Vector4 = Robust.Shared.Maths.Vector4;

namespace OpenDreamRuntime.Procs {
Expand Down Expand Up @@ -167,7 +169,7 @@ public static ProcStatus CreateObject(DMProcState state) {
var val = state.Pop();
if (!val.TryGetValueAsType(out var objectType)) {
if (val.TryGetValueAsString(out var pathString)) {
if (!state.Proc.ObjectTree.TryGetTreeEntry(new DreamPath(pathString), out objectType)) {
if (!state.Proc.ObjectTree.TryGetTreeEntry(pathString, out objectType)) {
ThrowCannotCreateUnknownObject(val);
}
} else {
Expand Down Expand Up @@ -1713,6 +1715,83 @@ async void Wait() {
return ProcStatus.Continue;
}

public static ProcStatus Sleep(DMProcState state) {
state.Pop().TryGetValueAsFloat(out var delay);
return SleepCore(
state,
state.ProcScheduler.CreateDelay(delay));
}

public static ProcStatus BackgroundSleep(DMProcState state) => SleepCore(
state,
state.ProcScheduler.CreateDelayTicks(-1));

static ProcStatus SleepCore(DMProcState state, Task delay) {
if (delay.IsCompleted)
return ProcStatus.Continue; // fast path, skip state creation

if (!SleepState.Pool.TryPop(out var sleepState)) {
sleepState = new SleepState();
}

return sleepState.Initialize(state.Thread, state.Proc, delay);
}

// "proc state" we just need something to hold the delay task
sealed class SleepState : AsyncProcState {
public static readonly Stack<SleepState> Pool = new();

[Dependency] public readonly ProcScheduler ProcScheduler = null!;

DreamProc? _proc;
Task? _task;
bool inResume;

public SleepState() {
IoCManager.InjectDependencies(this);
}

public ProcStatus Initialize(DreamThread thread, DMProc proc, Task delay) {
Thread = thread;
_proc = proc;
_task = ProcScheduler.Schedule(this, delay);
thread.PushProcState(this);
return thread.HandleDefer();
}

public override void Dispose() {
base.Dispose();
Thread = null!;
_proc = null;
_task = null;
Pool.Push(this);
}

public override DreamProc? Proc => _proc;

public override void AppendStackFrame(StringBuilder builder) {
builder.Append("/proc/sleep");
}

// a sleep is always the top of a thread so it's always safe to resume
public override void SafeResume() => Thread.Resume();

public override ProcStatus Resume() {
if (_task!.IsCompleted) {
// read before we get disposed when popped off
var exception = _task.Exception;
Thread.PopProcState();
if (exception != null) {
throw exception;
}

return ProcStatus.Returned;
}

return Thread.HandleDefer();
}
}

public static ProcStatus DebuggerBreakpoint(DMProcState state) {
return state.DebugManager.HandleBreakpoint(state);
}
Expand Down
2 changes: 2 additions & 0 deletions OpenDreamRuntime/Procs/DMProc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,8 @@ public sealed class DMProcState : ProcState {
{DreamProcOpcode.Locate, DMOpcodeHandlers.Locate},
{DreamProcOpcode.IsNull, DMOpcodeHandlers.IsNull},
{DreamProcOpcode.Spawn, DMOpcodeHandlers.Spawn},
{DreamProcOpcode.Sleep, DMOpcodeHandlers.Sleep},
{DreamProcOpcode.BackgroundSleep, DMOpcodeHandlers.BackgroundSleep},
{DreamProcOpcode.OutputReference, DMOpcodeHandlers.OutputReference},
{DreamProcOpcode.Output, DMOpcodeHandlers.Output},
{DreamProcOpcode.JumpIfNullDereference, DMOpcodeHandlers.JumpIfNullDereference},
Expand Down
1 change: 0 additions & 1 deletion OpenDreamRuntime/Procs/Native/DreamProcNative.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ public static void SetupNativeProcs(DreamObjectTree objectTree) {
objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_round);
objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_sha1);
objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_shutdown);
objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_sleep);
objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_sorttext);
objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_sorttextEx);
objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_sound);
Expand Down
10 changes: 0 additions & 10 deletions OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2145,16 +2145,6 @@ public static DreamValue NativeProc_shutdown(NativeProc.Bundle bundle, DreamObje
return DreamValue.Null;
}

[DreamProc("sleep")]
[DreamProcParameter("Delay", Type = DreamValueTypeFlag.Float)]
public static async Task<DreamValue> NativeProc_sleep(AsyncNativeProc.State state) {
state.GetArgument(0, "Delay").TryGetValueAsFloat(out float delay);

await state.ProcScheduler.CreateDelay(delay);

return DreamValue.Null;
}

[DreamProc("sorttext")]
[DreamProcParameter("T1", Type = DreamValueTypeFlag.String)]
[DreamProcParameter("T2", Type = DreamValueTypeFlag.String)]
Expand Down
1 change: 1 addition & 0 deletions OpenDreamRuntime/Procs/ProcDecoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ public ITuple DecodeInstruction() {
case DreamProcOpcode.PickWeighted:
case DreamProcOpcode.PickUnweighted:
case DreamProcOpcode.Spawn:
case DreamProcOpcode.Sleep:
case DreamProcOpcode.BooleanOr:
case DreamProcOpcode.BooleanAnd:
case DreamProcOpcode.SwitchCase:
Expand Down
20 changes: 15 additions & 5 deletions OpenDreamRuntime/Procs/ProcScheduler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,32 @@
namespace OpenDreamRuntime.Procs;

public sealed partial class ProcScheduler {
private readonly HashSet<AsyncNativeProc.State> _sleeping = new();
private readonly Queue<AsyncNativeProc.State> _scheduled = new();
private AsyncNativeProc.State? _current;
private readonly HashSet<AsyncProcState> _sleeping = new();
private readonly Queue<AsyncProcState> _scheduled = new();
private AsyncProcState? _current;

public bool HasProcsQueued => _scheduled.Count > 0 || _deferredTasks.Count > 0;

public Task Schedule(AsyncNativeProc.State state, Func<AsyncNativeProc.State, Task<DreamValue>> taskFunc) {
async Task Foo() {
state.Result = await taskFunc(state);
if (!_sleeping.Remove(state))
}

return Schedule(
state,
Foo());
}

public Task Schedule(AsyncProcState state, Task asyncTask) {
async Task Bar() {
await asyncTask;
if(!_sleeping.Remove(state))
return;

_scheduled.Enqueue(state);
}

var task = Foo();
var task = Bar();
if (!task.IsCompleted) // No need to schedule the proc if it's already finished
_sleeping.Add(state);

Expand Down
1 change: 1 addition & 0 deletions OpenDreamShared/Compiler/Token.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ public enum TokenType : byte {
DM_Slash,
DM_SlashEquals,
DM_Spawn,
DM_Sleep,
DM_Star,
DM_StarEquals,
DM_StarStar,
Expand Down

0 comments on commit 86fb394

Please sign in to comment.