xkkmEl Posted October 4, 2022 Share Posted October 4, 2022 As I am modding and trying to build up more and more convoluted story lines, I am finding myself more and more constrained and confused by the strictly linear quest structure, and a mess of crisscrossing events. All too often I am adding spinlocks around large code segments and dealing with piles of threads spinning their wheels while waiting for their turn. I grew tired of all that and decided to try something radically different: petrinets. So I am using endlessly looping scenes without actors or actions, only conditions and one-line script fragments, to manage scenes (the real kind with actors and actions) flowing into one another (in petrinet terms: scenes are places, script function calls are processes, execution policy is determined by the 'master' scene that has no actors or actions). This is working really well! I'm actually amazed. To make that work, I also needed better time/timeout management, so I made myself a simple delayed "ModEvent.send" script. Also very enjoyable to work with. Everything runs linearly, without any form of locking. If the code needs to wait for anything, it will not be a lock, but the next screen frame so that the conditions are reevaluated in the 'master' scene. Getting rid of spin locks really helps cut down on the noise and makes response times much more predictable, smooth, controllable. So I'm now going into fanatical mode to go without locking and spinlocks. I wrote myself a microkernel, in papyrus, in order to manage message-based "objects" (bits of data in a objid-objstate JMap). So I have little state machines to track all kinds of things and effect changes in the world on command, in sequence. I use them to avoid repeating the same scenes too often, manage outfits, implement group equivalents to registerForSingleUpdate, elect a volunteers from a group of registrants, etc. I am now at a point where I am considering adding a Forth interpreter! I have cases where I want to wait for a modevent to finish running, which would require it can signal back to his caller. Can't think of a 'reasonable' way to do it, without introducing programmable finite state automatons into the mix. The Forth approach looks to me the most reasonable in implementation and generality. PS: I'm not using any SKSE plugins for this, except for the JContainer stuff on which all this is built. It's all papyrus code, leveraging modevents, mostly in chains where each mod event call (after its done its work) calls back to a global JContainer data structure that directs it to issue other modevents. In this way, you create execution threads that are highly versatile and, because the modevents run sequentially, require no locking. PPS: Well... I also depend on JArray.add* beging atomic, as well as JMap.setInt and JMap.removeKey. Nada mas. Link to comment Share on other sites More sharing options...
showler Posted October 4, 2022 Share Posted October 4, 2022 That last sentence was the only part of this I came close to understanding. Link to comment Share on other sites More sharing options...
IsharaMeradin Posted October 4, 2022 Share Posted October 4, 2022 I'll have to go with showler on this one. I read it and don't really understand what you said. Does that make you off your rockers? Who knows. But hey, if it works for you and it isn't breaking anything else in the game, go for it... I suppose. Link to comment Share on other sites More sharing options...
xkkmEl Posted October 5, 2022 Author Share Posted October 5, 2022 (edited) In case anyone wants to know more on my experiment... At the core, is the idea of a microkernel: a simple queue of work items to process. I use the symmetry between ModEvent.push* and JArray.add* to store a pending "modevent" in a JArray, and then push them onto another JArray that implements the queue itself. int queue = JValue.retain( JArray.object) int ev = JValue.retain( JArray.object) JArray.addStr( ev, "mymodeventname") JArray.addInt( ev, 1) JArray.addObj( queue, ev) JValue.release( ev) Then, instead of having a processing loop, I just have each work item (the modevent handler) finish with a call that checks for more queued items and starts the next one. function process() if !active && JArray.count( queue) active = True int ev = JArray.getObj( queue, 0) JArray.eraseIndex( queue, 0) sendModEvent( ev) endif endfunction event OnMyModEventHandler( string eventName, int arg) doWhatEver() if JArray.count( queue) int ev = JArray.getObj( queue, 0) JArray.eraseIndex( queue, 0) sendModEvent( ev) else active = False endif endevent You can then expand on that to manage a collection of parallel modevent chains of that sort. So I have kernelRequest (in that global queue), that I use to implement:state machines (which have a state and process events sequentially), and looking to implement:forth interpreters (which have a program, program counter and working stack. Edited October 5, 2022 by xkkmEl Link to comment Share on other sites More sharing options...
xkkmEl Posted October 19, 2022 Author Share Posted October 19, 2022 I am happy to report that the forth-like state machine is working very well, when it doesn't CTD. Link to comment Share on other sites More sharing options...
xkkmEl Posted October 20, 2022 Author Share Posted October 20, 2022 (edited) So, I am now almost a week into hunting the source of my CTDs. I have a reproducible method for triggering it. I have traced painfully up to a point where my code is setting up two modevents to be triggered virtually at the same time to access the same JArray. Tracing these two pathes without interfering with the timing is tricky, and I am not sure how to get closer to the faulty code. However, it points to a defect in concurrent accesses to a JArray, within the JContainers SKSE plugin. I thought I'd be dealing with a write/write conflict, but reading the JContainers c++ code, I see that all writes seem to be properly protected by a libboost mutex, which I assume understands the Windows threading model and is effective no matter what Skyrim does. The read-only api calls however do not seem to be protected by a mutex. My current theory is that I am seeing a read/write conflict in which the write does a structural change to the JArray (reallocates it), and that the "thread safety" promises of JContainers are flawed. If anybody has any pointers that could help, they will be appreciated. Otherwise, I'll start looking for a way to bypass this flaw, but I am not seeing any good options. Edited October 20, 2022 by xkkmEl Link to comment Share on other sites More sharing options...
xkkmEl Posted October 25, 2022 Author Share Posted October 25, 2022 So I finally have a handle on that CTD. Not a concurrency problem; I made my code linear, single threaded, and I still had the problem. It turns out that JArray sometimes CTDs on out of bound reads. I didn't trace exactly where it crashes inside my loop, so I don't know which of the following calls is doing it: .valueType, .getInt, .getFlt, .getForm, .getString, .getObj. The only other code in the loop is string concatenation (x += "string expression") and I was not doing enough of them to bust the memory. Link to comment Share on other sites More sharing options...
xkkmEl Posted November 15, 2022 Author Share Posted November 15, 2022 After much trial and error, I came to the conclusion that modevents have serious bandwidth issues. I've changed my approach to use per-function-quests instead of modevents to implement function libraries. I now have a gazillion trivial quests. In my testing, it appeared that modevents are only sent at screen frame boundaries; that is, a modevent that ends by sending another mod event, and so forth, can only process 20 calls per second if you have a frame rate of 20fps. Worst, the ModEvent.send function itself blocks until that screen frame transition, making it difficult for one modevent to trigger two in order to scale up processing speed when needed. Worst even, all the modevents trigger at the same time, causing much contention for the CPU and locking world objects. This results in a greatly lowered frame rate and a cyclic jittery user experience. That's without counting when everything stops working because the game decided to dump stacks (suspend all sorts of unrelated threads while trying to work through all the running and waiing scripts). ModEvents are still great for low-bandwidth, low-coupling interfaces, but not for a microkernel or function library that will get heavy use. Link to comment Share on other sites More sharing options...
Recommended Posts