Skip to content
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

Fixes yield proc scheduling. Turns sleeps into an opcode #1539

Draft
wants to merge 15 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions Content.Tests/ContentUnitTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
using System.Reflection;
using OpenDreamClient;
using OpenDreamRuntime;
using OpenDreamRuntime.Procs;
using OpenDreamRuntime.Rendering;
using OpenDreamShared;
using OpenDreamShared.Rendering;
using Robust.Shared.Analyzers;
using Robust.Shared.IoC;
Expand All @@ -23,11 +23,10 @@ public class ContentUnitTest : RobustUnitTest {
protected override void OverrideIoC() {
base.OverrideIoC();

SharedOpenDreamIoC.Register();

if (Project == UnitTestProject.Server) {
ServerContentIoC.Register(unitTests: true);
IoCManager.Register<IDreamMapManager, DummyDreamMapManager>();
IoCManager.Register<IOpenDreamGameTiming, DummyOpenDreamGameTiming>();
} else if (Project == UnitTestProject.Client) {
ClientContentIoC.Register();
}
Expand Down
20 changes: 20 additions & 0 deletions Content.Tests/DMProject/Tests/Sleeping/Basic.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// RETURN TRUE

var/should_be_set = FALSE

/proc/RunTest()
ASSERT(world.time == 0)
sleep(world.tick_lag)
ASSERT(world.time == 1)
sleep(10)
ASSERT(world.time == 11)
StackCheck()
ASSERT(world.time == 61)
ASSERT(should_be_set)
return TRUE

/proc/StackCheck()
ASSERT(world.time == 11)
sleep(50)
ASSERT(world.time == 61)
should_be_set = TRUE
58 changes: 58 additions & 0 deletions Content.Tests/DMProject/Tests/Sleeping/YieldOrder.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
var/counter
#define ExpectOrder(n) ASSERT(++counter == ##n)

/proc/BackgroundSleep(delay, expect)
set waitfor = FALSE
sleep(delay)
world.log << "Expect: [expect]"
ExpectOrder(expect)

#define MODE_INLINE 0 // spawn
#define MODE_BACKGROUND 1 // set waitfor = FALSE + sleep
#define MODE_RAND 2 // random seeded

#define TestSleep(delay, expect) if(mode == MODE_INLINE || (mode == MODE_RAND && prob(50))){ spawn(##delay) { ExpectOrder(##expect); } } else { BackgroundSleep(##delay, ##expect); }

/proc/TestSequence(mode)
counter = 0
var/start_tick = world.time

TestSleep(0, 2)
ExpectOrder(1)
sleep(0)
ExpectOrder(3)

TestSleep(-1, 4)
ExpectOrder(5)

TestSleep(0, 6)
sleep(-1)
ExpectOrder(7)

TestSleep(-1, 8)
ExpectOrder(9)
sleep(-1)
ExpectOrder(10)

TestSleep(1, 13)
sleep(-1)
ExpectOrder(11)
sleep(0)
ExpectOrder(12)

ASSERT(world.time == start_tick)

sleep(1)
ExpectOrder(14)

/proc/RunTest()
world.log << "Inline:"
TestSequence(MODE_INLINE)

world.log << "Background:"
TestSequence(MODE_BACKGROUND)

rand_seed(22475)
for(var/i in 1 to 10000)
world.log << "Rand-[i]:"
TestSequence(MODE_RAND)
Comment on lines +55 to +58
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had to up the test timeout because this overran 500ms

20 changes: 18 additions & 2 deletions Content.Tests/DMTests.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using OpenDreamRuntime;
using OpenDreamRuntime.Objects;
using OpenDreamRuntime.Procs;
using Robust.Shared.Asynchronous;
using Robust.Shared.IoC;
using Robust.Shared.Timing;
Expand All @@ -17,10 +19,14 @@ public sealed class DMTests : ContentUnitTest {
private const string InitializeEnvironment = "./environment.dme";
private const string TestsDirectory = "Tests";

[Dependency] IOpenDreamGameTiming _gameTiming = default!;
[Dependency] private readonly DreamManager _dreamMan = default!;
[Dependency] private readonly DreamObjectTree _objectTree = default!;
[Dependency] private readonly ProcScheduler _procScheduler = default!;
[Dependency] private readonly ITaskManager _taskManager = default!;

DummyOpenDreamGameTiming GameTiming => (DummyOpenDreamGameTiming)_gameTiming;

[Flags]
public enum DMTestFlags {
NoError = 0, // Should run without errors
Expand Down Expand Up @@ -70,16 +76,22 @@ public void TestFiles(string sourceFile, DMTestFlags testFlags) {
return;
}

_procScheduler.ClearState();
GameTiming.CurTick = GameTick.Zero;

Assert.That(compiledFile is not null && File.Exists(compiledFile), "Failed to compile DM source file");
Assert.That(_dreamMan.LoadJson(compiledFile), $"Failed to load {compiledFile}");
_dreamMan.StartWorld();

(bool successfulRun, DreamValue? returned, Exception? exception) = RunTest();

int expectedThreads;
if (testFlags.HasFlag(DMTestFlags.NoReturn)) {
Assert.That(returned.HasValue, Is.False, "proc returned unexpectedly");
expectedThreads = 1;
} else {
Assert.That(returned.HasValue, "proc did not return (did it hit an exception?)");
expectedThreads = 0;
}

if (testFlags.HasFlag(DMTestFlags.RuntimeError)) {
Expand All @@ -95,6 +107,9 @@ public void TestFiles(string sourceFile, DMTestFlags testFlags) {
Assert.That(returned?.IsTruthy(), Is.True, "Test was expected to return TRUE");
}

var threads = _procScheduler.InspectThreads().ToList();
Assert.That(threads.Count == expectedThreads && !_procScheduler.HasProcsSleeping && !_procScheduler.HasProcsQueued, $"One or more threads did not finish!");

Cleanup(compiledFile);
TestContext.WriteLine($"--- PASS {sourceFile}");
} finally {
Expand Down Expand Up @@ -124,11 +139,12 @@ public void TestFiles(string sourceFile, DMTestFlags testFlags) {
watch.Start();

// Tick until our inner call has finished
while (!callTask.IsCompleted) {
while (!callTask.IsCompleted || _procScheduler.HasProcsQueued || _procScheduler.HasProcsSleeping) {
GameTiming.CurTick = new GameTick(_gameTiming.CurTick.Value + 1);
_dreamMan.Update();
_taskManager.ProcessPendingTasks();

if (watch.Elapsed.TotalMilliseconds > 500) {
if (GameTiming.CurTick.Value > 50000) {
Assert.Fail("Test timed out");
}
}
Expand Down
26 changes: 26 additions & 0 deletions Content.Tests/DummyOpenDreamGameTiming.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using OpenDreamRuntime.Procs;

using Robust.Shared.IoC;
using Robust.Shared.Timing;

using System;

namespace Content.Tests {
sealed class DummyOpenDreamGameTiming : IOpenDreamGameTiming {

[Dependency] IGameTiming _gameTiming = null!;

public GameTick CurTick { get; set; }

public TimeSpan LastTick => _gameTiming.LastTick;

public TimeSpan RealTime => _gameTiming.RealTime;

public TimeSpan TickPeriod => _gameTiming.TickPeriod;

public byte TickRate {
get => _gameTiming.TickRate;
set => _gameTiming.TickRate = value;
}
}
}
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 {
[OpcodeMetadata(stackDelta: -1)] Log = 0x81,
LogE = 0x82,
Abs = 0x83,
[OpcodeMetadata(stackDelta: -1)] Sleep = 0x84,
BackgroundSleep = 0x85,
}

/// <summary>
Expand Down
7 changes: 7 additions & 0 deletions DMCompiler/Compiler/DM/AST/DMAST.ProcStatements.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using DMCompiler.DM;
using DMCompiler.DM.Expressions;

namespace DMCompiler.Compiler.DM.AST;

Expand Down Expand Up @@ -80,6 +81,12 @@ public sealed class DMASTProcStatementSet(
public readonly bool WasInKeyword = wasInKeyword; // Marks whether this was a "set x in y" expression, or a "set x = y" one
}

public sealed class DMASTProcStatementSleep(
Location location,
DMASTExpression delay) : DMASTProcStatement(location) {
public DMASTExpression Delay = delay;
}

public sealed class DMASTProcStatementSpawn(Location location, DMASTExpression delay, DMASTProcBlockInner body)
: DMASTProcStatement(location) {
public DMASTExpression Delay = delay;
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 @@ -58,6 +58,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 @@ -95,6 +95,7 @@
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 @@ -157,7 +158,7 @@
List<DMASTStatement>? blockInner = BlockInner();

if (blockInner != null) statements.AddRange(blockInner);
} catch (CompileErrorException) { }

Check warning on line 161 in DMCompiler/Compiler/DM/DMParser.cs

View workflow job for this annotation

GitHub Actions / build

'CompileErrorException' is obsolete: 'This is not a desirable way for the compiler to emit an error. Use CompileAbortException or ForceError() if it needs to be fatal, or an DMCompiler.Emit() otherwise.'

if (Current().Type != TokenType.EndOfFile) {
Token skipFrom = Current();
Expand Down Expand Up @@ -186,7 +187,7 @@
} else {
if (statements.Count == 0) return null;
}
} catch (CompileErrorException) {

Check warning on line 190 in DMCompiler/Compiler/DM/DMParser.cs

View workflow job for this annotation

GitHub Actions / build

'CompileErrorException' is obsolete: 'This is not a desirable way for the compiler to emit an error. Use CompileAbortException or ForceError() if it needs to be fatal, or an DMCompiler.Emit() otherwise.'
LocateNextStatement();
}
} while (Delimiter());
Expand Down Expand Up @@ -753,6 +754,7 @@
procStatement ??= Switch();
procStatement ??= Continue();
procStatement ??= Break();
procStatement ??= Sleep();
procStatement ??= Spawn();
procStatement ??= While();
procStatement ??= DoWhile();
Expand Down Expand Up @@ -1097,6 +1099,23 @@
}
}

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
1 change: 1 addition & 0 deletions DMCompiler/Compiler/Token.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ public enum TokenType : byte {
DM_Slash,
DM_SlashEquals,
DM_Spawn,
DM_Sleep,
DM_Star,
DM_StarEquals,
DM_StarStar,
Expand Down
4 changes: 4 additions & 0 deletions DMCompiler/DM/Builders/DMASTFolder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ public void FoldAst(DMASTNode? ast) {
statementSpawn.Delay = FoldExpression(statementSpawn.Delay);
FoldAst(statementSpawn.Body);

break;
case DMASTProcStatementSleep statementSleep:
statementSleep.Delay = FoldExpression(statementSleep.Delay);

break;
case DMASTProcStatementBrowse statementBrowse:
statementBrowse.Receiver = FoldExpression(statementBrowse.Receiver);
Expand Down
16 changes: 16 additions & 0 deletions DMCompiler/DM/Builders/DMProcBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
//Set default
try {
DMExpression.Emit(_dmObject, _proc, parameter.Value, parameter.ObjectType);
} catch (CompileErrorException e) {

Check warning on line 48 in DMCompiler/DM/Builders/DMProcBuilder.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

'CompileErrorException' is obsolete: 'This is not a desirable way for the compiler to emit an error. Use CompileAbortException or ForceError() if it needs to be fatal, or an DMCompiler.Emit() otherwise.'
DMCompiler.Emit(e.Error);
}
_proc.Assign(parameterRef);
Expand Down Expand Up @@ -117,6 +117,7 @@
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 @@ -372,6 +373,21 @@
_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
21 changes: 10 additions & 11 deletions DMCompiler/DM/DMProc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -440,20 +440,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 <= 0) // 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
1 change: 0 additions & 1 deletion DMCompiler/DMStandard/_Standard.dm
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,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
1 change: 0 additions & 1 deletion OpenDreamClient/Rendering/ClientImagesSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
namespace OpenDreamClient.Rendering;
internal sealed class ClientImagesSystem : SharedClientImagesSystem {
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly ClientAppearanceSystem _appearanceSystem = default!;

private readonly Dictionary<Vector3, List<NetEntity>> _turfClientImages = new();
Expand Down
2 changes: 1 addition & 1 deletion OpenDreamRuntime/DreamManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public sealed partial class DreamManager {
[Dependency] private readonly ProcScheduler _procScheduler = default!;
[Dependency] private readonly DreamResourceManager _dreamResourceManager = default!;
[Dependency] private readonly ITaskManager _taskManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IOpenDreamGameTiming _gameTiming = default!;
[Dependency] private readonly DreamObjectTree _objectTree = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
Expand Down
Loading
Loading