Spherical forums

Creations => Programming => Topic started by: Fat Cerberus on February 14, 2015, 03:50:29 pm

Title: What's wrong here?
Post by: Fat Cerberus on February 14, 2015, 03:50:29 pm
Okay, I'm completely stumped here.  As it turns out, the Spectacles threader doesn't run on minisphere.  When it comes time to render or update threads, I get errors because, for some reason, the thread objects are being scrambled and some are coming back undefined.  At first I suspected a Duktape bug, but as it turns out, guess what?  The same exact thing happens, in the same exact place, when run in Sphere-SFML!  This obviously isn't a coincidence; SpiderMonkey in Sphere 1.5 implements ES3 while both Duktape and Jurassic are ES5-compliant.  Meaning there's something in there that's screwing up under ES5, but I can't figure out what, and I've combed the code over a dozen times.

Maybe somebody else could figure it out?  Here's the source for the Specs threader:

Code: (javascript) [Select]
/***
* Specs Engine v6: Spectacles Saga Game Engine
  *           Copyright (C) 2012 Power-Command
***/

// Threads object
// Represents the Specs Engine thread manager.
Threads = new (function()
{
this.hasUpdated = false;
this.nextThreadID = 1;
this.threads = [];
})();

// .initialize() method
// Initializes the thread manager.
Threads.initialize = function()
{
this.threadSorter = function(a, b) {
return a.priority != b.priority ?
a.priority - b.priority :
a.id - b.id;
};
}

// .create() method
// Creates a thread and begins running it.
// Arguments:
//     o:            The object to pass as 'this' to the updater/renderer/input handler. May be null.
//     updater:      The update function for the new thread.
//     renderer:     Optional. The render function for the new thread.
//     priority:     Optional. The render priority for the new thread. Higher-priority threads are rendered
//                   later in a frame than lower-priority ones. Ignored if no renderer is provided. (default: 0)
//     inputHandler: Optional. The input handler for the new thread.
Threads.create = function(o, updater, renderer, priority, inputHandler)
{
renderer = renderer !== void null ? renderer : null;
inputHandler = inputHandler !== void null ? inputHandler : null;
priority = priority !== void null ? priority : 0;

updater = updater.bind(o);
renderer = renderer !== null ? renderer.bind(o) : null;
inputHandler = inputHandler !== null ? inputHandler.bind(o) : null;
var newThread = {
id: this.nextThreadID,
inputHandler: inputHandler,
isUpdating: false,
priority: priority,
renderer: renderer,
updater: updater
};
this.threads.push(newThread);
++this.nextThreadID;
return newThread.id;
};

// .createEntityThread() method
// Creates a thread for a specified entity.
// Arguments:
//     entity:   The entity for which to create the thread. This should be an object having .update() and
//               optionally, .render() and .getInput() methods. Each of these will be called once
//               per frame until the thread either finishes (entity.update() returns false) or is terminated.
//     priority: Optional. The render priority for the new thread. Higher-priority threads are rendered
//               later in a frame than lower-priority ones. Ignored if no renderer is provided. (default: 0)
Threads.createEntityThread = function(entity, priority)
{
priority = priority !== void null ? priority : 0;

var updater = entity.update;
var renderer = (typeof entity.render === 'function') ? entity.render : null;
var inputHandler = (typeof entity.getInput === 'function') ? entity.getInput : null;
return this.create(entity, updater, renderer, priority, inputHandler);
};

// .doFrame() method
// Performs update and render processing for a single frame.
Threads.doFrame = function()
{
if (IsMapEngineRunning()) {
RenderMap();
} else {
this.renderAll();
}
FlipScreen();
if (IsMapEngineRunning()) {
this.hasUpdated = false;
UpdateMapEngine();
if (!this.hasUpdated) {
this.updateAll();
}
} else {
this.updateAll();
}
};

// .doWith() method
// Creates an improvised thread running in the context of a specified object.
// Arguments:
//     o:            The object to pass as 'this' to the updater/renderer.
//     updater:      The update function for the new thread.
//     renderer:     Optional. The render function for the new thread.
//     priority:     Optional. The render priority for the new thread. Higher-priority threads are rendered
//                   later in a frame (closer to the player) than lower-priority ones. (default: 0)
//     inputHandler: Optional. The input handler for the new thread.
Threads.doWith = function(o, updater, renderer, priority, inputHandler)
{
return this.create(o, updater, renderer, priority, inputHandler);
};

// .isRunning() method
// Determines whether a thread is still running.
// Arguments:
//     threadID: The ID of the thread to check.
Threads.isRunning = function(threadID)
{
if (threadID === 0) {
return false;
}
for (var i = 0; i < this.threads.length; ++i) {
if (this.threads[i].id == threadID) {
return true;
}
}
return false;
};

// .kill() method
// Prematurely terminates a thread.
// Arguments:
//     threadID: The ID of the thread to terminate.
Threads.kill = function(threadID)
{
for (var i = 0; i < this.threads.length; ++i) {
if (threadID == this.threads[i].id) {
this.threads.splice(i, 1);
--i;
}
}
};

// .renderAll() method
// Renders the current frame by calling all active threads' renderers.
Threads.renderAll = function()
{
Link(Link(this.threads).sort(this.threadSorter))
.where(function(thread) { return thread.renderer !== null })
.each(function(thread)
{
thread.renderer();
});
};

// .synchronize() method
// Waits for multiple threads to finish.
// Arguments:
//     threadIDs: An array of thread IDs specifying the threads to wait for.
Threads.synchronize = function(threadIDs)
{
var isFinished;
do {
this.doFrame();
isFinished = true;
for (var i = 0; i < threadIDs.length; ++i) {
isFinished = isFinished && !this.isRunning(threadIDs[i]);
}
} while (!isFinished);
};

// .updateAll() method
// Updates all active threads for the next frame.
Threads.updateAll = function()
{
var threadsEnding = [];
Link(Link(this.threads).sort(this.threadSorter))
.each(function(thread)
{
var stillRunning = true;
if (!thread.isUpdating) {
thread.isUpdating = true;
stillRunning = thread.updater();
if (thread.inputHandler !== null && stillRunning) {
thread.inputHandler();
}
thread.isUpdating = false;
}
if (!stillRunning) {
threadsEnding.push(thread.id);
}
});
for (var i = 0; i < threadsEnding.length; ++i) {
this.kill(threadsEnding[i]);
}
this.hasUpdated = true;
};

// .waitFor() method
// Waits for a thread to terminate.
// Arguments:
//     threadID: The ID of the thread to wait for.
Threads.waitFor = function(threadID)
{
var i = 0;
while (this.isRunning(threadID)) {
this.doFrame();
}
};
Title: Re: What's wrong here?
Post by: Fat Cerberus on February 14, 2015, 07:24:56 pm
So after further testing, I think it might just be a bug in Link that's causing this.  See, if I change this:

Code: (javascript) [Select]
Link(Link(this.threads).sort(this.threadSorter))

to this:
Code: (javascript) [Select]
Link(this.threads)


It works just fine, albeit with the threads rendered out of order.  Interestingly enough, however, the following ALSO causes the error to show up:
Code: (javascript) [Select]
Link(Link(this.threads).toArray())


Which should definitely not cause an issue since Link(this.threads) works normally.
Title: Re: What's wrong here?
Post by: Radnen on February 14, 2015, 10:09:13 pm
.sort calls the array.sort method, which might be implemented differently in SM v1.5 vs. other JS engines?

Code: (javascript) [Select]

function Sort(f) {
var v = this.toArray();
if (f) v.sort(f); else v.sort();
return v;
}


I would have made my own sort method, but for now the internal JS version was faster and, well, got the job done. I guess the other engines really are to blame? I'd give this a test. Test the array.sort method and see what happens under certain conditions. I think tie breakers are what's broken since the specs don't really govern that and it's really up to the implementer's judgement.
Title: Re: What's wrong here?
Post by: Fat Cerberus on February 14, 2015, 10:30:17 pm
As mentioned above, the error also happens if I replace the .sort() with .toArray().  So it's not specific to sorting, something's borked in Link:toArray (which Link:sort uses).
Title: Re: What's wrong here?
Post by: Flying Jester on February 15, 2015, 12:43:32 am
Just a cursory look, but I wonder if the issue is somewhere in here...
Code: (javascript) [Select]

function Run(point) {
this.env.stop = false;
this.env.skip = 0;
if (point) this.pushPoint(point);
var start = this.points[0];

// reset the points that store data between runs
for (var i = 0; i < this.points.length; ++i) {
var p = this.points[i];
if (p.reset) p.reset();
}

// kick-start points that have a runner tied to them:
if (start.run) { start.run(this.target); }
else {
var a = this.target, l = a.length, i = 0, e = this.env;
if (e.take)
while (i < l && !e.stop) start.exec(a[i++]);
else
while (i < l) start.exec(a[i++]);
}
this.points.length--;
}


Maybe Jurassic and Duktape don't appreciate decrementing the length of an Array that way?
Title: Re: What's wrong here?
Post by: Radnen on February 15, 2015, 02:07:20 am
No, it works correctly.

Code: (javascript) [Select]

var n = Link([0, 1, 2]).map(function(n) { return n+1; }).map(function(n) { return n-1; });
n.each(function() {});
n.each(function() {});
n.toArray();

Abort(n.points.length + ", looks like: " + n.toArray());


Changing the number of times n.each() is called, or n.toArray() doesn't affect the size of the array. That's why array.length - 1 is used, so you can keep the chain from n-1 and use different end points on it.
Title: Re: What's wrong here?
Post by: Fat Cerberus on February 15, 2015, 02:15:15 am
Found the bug!  For some reason toArray() is prepending an extra element to the array under ES5 engines.  See screenshot below.

Code: (javascript) [Select]
var foo = [ 5, 7, 3, 8, 1, 2, 4, 6 ];
var bar = Link(foo).toArray();
var baz = Link(bar).sort();
Abort("foo: [ " + foo.toString() + " ]\nbar: [ " + bar.toString() + " ]\n" + "baz: [ " + baz.toString() + " ]");
Title: Re: What's wrong here?
Post by: Radnen on February 15, 2015, 02:28:23 am
Fixed it.

old:
Code: (javascript) [Select]

AllPoint.prototype.run = function(a) {
var i = 0, l = a.length, b = this.array;
while (i < l) { b[i] = a[i++]; }
this.array = b;
}


new:
Code: (javascript) [Select]

AllPoint.prototype.run = function(a) {
var i = 0, l = a.length, b = this.array;
while (i < l) { b[i] = a[i]; i++; }
this.array = b;
}


I thought it would work correctly but unlike arithmetic, variable assignment goes the opposite way in the order of operations. So, I have to make sure incrementation happens after all is said and done just to make doubly sure.
Title: Re: What's wrong here?
Post by: Fat Cerberus on February 15, 2015, 02:30:20 am
How come it worked okay under Sphere 1.5?  Did ES5 change the operator precedence rules?
Title: Re: What's wrong here?
Post by: Radnen on February 15, 2015, 02:32:05 am
And, so yes, this fixes it too, and breaks it in Sphere 1.5! :P

Code: (javascript) [Select]

AllPoint.prototype.run = function(a) {
var i = 0, l = a.length, b = this.array;
while (i < l) { b[i++] = a[i]; }
this.array = b;
}


So, the order of ops on this is incorrect. I'll have to take a look to see what is correct, but I'm thinking the ECMA 5 engines are correct.
Title: Re: What's wrong here?
Post by: Flying Jester on February 15, 2015, 02:34:03 am
Does it work correctly in modern Chrome or Fx?

I wonder if it might be a bug with the SM 1.5 uses. If you upload a test SPK or game, I can test with some of my mac 1.6+ builds that have other versions of libjs (1.3-1.8 ).
Title: Re: What's wrong here?
Post by: Fat Cerberus on February 15, 2015, 02:37:26 am

And, so yes, this fixes it too, and breaks it in Sphere 1.5! :P

Code: (javascript) [Select]

AllPoint.prototype.run = function(a) {
var i = 0, l = a.length, b = this.array;
while (i < l) { b[i++] = a[i]; }
this.array = b;
}



Haha, no kidding. :D  That's actually even worse than the original bug--items get deleted!
Title: Re: What's wrong here?
Post by: Fat Cerberus on February 15, 2015, 02:40:07 am

Does it work correctly in modern Chrome or Fx?

I wonder if it might be a bug with the SM 1.5 uses. If you upload a test SPK or game, I can test with some of my mac 1.6+ builds that have other versions of libjs (1.3-1.8 ).


My minimal foo/bar/baz code above is enough of a test, you just need a game() function with that along with a RequireScript for link.js.  The bug is painfully obvious just by printing out the array.
Title: Re: What's wrong here?
Post by: Flying Jester on February 15, 2015, 03:32:46 am
Under TurboSphere with SM 37, using Link 0.2.15 from github:

Code: [Select]

foo: [ 5,7,3,8,1,2,4,6 ]
bar: [ 5,7,3,8,1,2,4,6 ]
baz: [ 1,2,3,4,5,6,7,8 ]


So I would suspect this is a bug, or two bugs with similar triggers, in both Duktape and Jurassic. SM 37 shares hardly anything with SM 1.8, and I am using the ES5/Harmony profile.
Title: Re: What's wrong here?
Post by: Radnen on February 15, 2015, 03:36:18 am
In Chrome, this doesn't work
Code: (javascript) [Select]

while (i < l) { b[i++] = a[i]; }


Maybe Duktape and Jurassic assumed the wrong order of operations? You'd think Google will have the correct method since as a web browser, it can't be allowed to break peoples code. To check, Firefox and IE produce same result.

So, that's weird!

Edit:
ninja'd by FJ, but I added more tests.

Edit 2:
I will say that in usual circumstances Jurassic and Duktape have the right of it. Variable assignment is usually right to left while other operations are left to right. However, it seems JavaScript wants it to go left to right as well, and the two ECMA5 engines just don't comply. Still don't know if it is an ECMA5 spec mishap or not.
Title: Re: What's wrong here?
Post by: Fat Cerberus on February 15, 2015, 09:58:50 am
Either way, performing the increment separately works in both engines, so at least I can run Specs on minisphere (and probably SSFML) now!  Curious that both Duktape and Jurassic exhibit the exact same inconsistency, though. :o
Title: Re: What's wrong here?
Post by: N E O on February 15, 2015, 02:42:23 pm
One of the reasons I write my loops { i = -1; while (++i < l) { ... } }
Title: Re: What's wrong here?
Post by: Fat Cerberus on February 15, 2015, 03:57:36 pm
I won't even do that.  I despise inline increment/decrement (well, anything with side effects, really), I find code that does so to be confusing.  In general, I will always prefer readability over compactness, regardless of language.  I have found that I tend to do more questionable stuff in C though, not sure why...
Title: Re: What's wrong here?
Post by: Fat Cerberus on April 12, 2015, 02:32:11 am

I won't even do that.  I despise inline increment/decrement (well, anything with side effects, really), I find code that does so to be confusing.  In general, I will always prefer readability over compactness, regardless of language.  I have found that I tend to do more questionable stuff in C though, not sure why...


Wow, I do this all over the place in minisphere now.  I really need to shut up sometimes. :-[

[karkat]PAST ME IS ALWAYS SO TERRIBLE, EVEN WHEN I LITERALLY JUST FINISHED BEING HIM.[/karkat]
Title: Re: What's wrong here?
Post by: Radnen on April 12, 2015, 02:55:22 am

[karkat]PAST ME IS ALWAYS SO TERRIBLE, EVEN WHEN I LITERALLY JUST FINISHED BEING HIM.[/karkat]


Tell me about it. If I could get a penny for things I regret saying in the past, I'd be, well I'd have a few bucks in my pocket. :P