Difference between revisions of "User:Becky Pippen"

From Second Life Wiki
Jump to navigation Jump to search
m
(Remove link to page of obsolete script timing measurements.)
 
(5 intermediate revisions by the same user not shown)
Line 19: Line 19:
  Other languages (future)  ---> other compiler |
  Other languages (future)  ---> other compiler |
=== Do Scripts Running in Mono Get More Memory? ===
=== Do Scripts Running in Mono Get More Memory? ===
Yes, it's true that each Mono script gets 64K of memory to use. Non-Mono scripts have to fit inside 16K of memory. (That's compiled code plus stack plus heap.) Unfortunately, Mono-compiled code is a bit of a memory hog and can take up to 4 times as much memory as the old runtime environment. So, scripts using Mono end up with a little more memory to play with depending on the code-to-data ratio, but not four times as much. The advantage to the servers is that small Mono scripts only consume as much server memory as they actually use, while the older LSL virtual machine had to allocate a chunk of 16K of memory to every script regardless of how little memory it actually needed.  
Yes, it's true that each Mono script gets 64K of memory to use. Non-Mono scripts have to fit inside 16K of memory. (That's compiled code plus stack plus heap.) Unfortunately, Mono-compiled code is a bit of a memory hog and can take up to 4 times as much memory as the old runtime environment. So, scripts using Mono end up with a little more memory to play with depending on the code-to-data ratio, but not four times as much.
=== Evolving Terminology ===
=== Evolving Terminology ===
LSL - Used to refer to the whole system -- the language, the front-end compiler, and the back-end interpreter. Now it's confusing because it's used in one of two ways and its meaning depends on the context:  (1) LSL is the language, the source code, which has not changed with Mono. E.g., "Take this LSL script and compile it with Mono and it will run faster." Or (2) LSL refers to the old compiler front-end and interpreter virtual machine back-end. E.g., "When I compile this script with LSL it runs very slowly." Both meanings are used in the sentence, "This LSL script was compiled with LSL, but that LSL script was compiled with Mono."
LSL - Used to refer to the whole system -- the language, the front-end compiler, and the back-end interpreter. Now it's confusing because it's used in one of two ways and its meaning depends on the context:  (1) LSL is the language, the source code, which has not changed with Mono. E.g., "Take this LSL script and compile it with Mono and it will run faster." Or (2) LSL refers to the old compiler front-end and interpreter virtual machine back-end. E.g., "When I compile this script with LSL it runs very slowly." Both meanings are used in the sentence, "This LSL script was compiled with LSL, but that LSL script was compiled with Mono."
Line 36: Line 36:


* '''''C# and other languages:''''' probably added at some future time.
* '''''C# and other languages:''''' probably added at some future time.
== LSL Timings and Rates ==
Here are some LSL function times and event rates comparing scripts compiled and run using the old LSL toolset and using Mono.
The basic test framework for these tests is to set i to a large value, and take the difference in llGetTime() before and after the loop shown below (the loop is partially unrolled to make the loop control structure overhead negligible), and take several measurements in multiple Class 5 sims that are running consistently with a total frame time under 10 ms, where each test runs for 30 - 120 minutes, and measured several times during a 24-hour period until the numbers converge with a variance of 10% or less:
while (--i >= 0) {
    f(); f(); f(); f(); f(); f(); f(); f();
    f(); f(); f(); f(); f(); f(); f(); f();
}
In a Mono sim, the result of the first run after script reset is ignored to eliminate the overhead of the VM initialization, JIT compilation, etc.
Note that these are stopwatch times. How rapidly you can call a function or how often an event handler is invoked doesn't necessarily correspond to how much those functions and events lag a sim because of all the throttling and scheduling going on behind the scenes inside the run-time engine.
=== Functions, operators, & control structures ===
{| cellpadding="2" cellspacing="0" border="1"
|-
|align="center" width="300" rowspan="2"|call to empty f() { }
|align="center" width="80"|LSL
|align="center" width="80"|Mono
|align="center" width="80"|Units
|-
|align="center"|0.33
|align="center"|0.0026
|align="center"|ms/call
|}
{| cellpadding="2" cellspacing="0" border="1"
|-
|align="center" width="300" rowspan="2"|for loop overhead
for (i = 0; i == 0; ++i) { }
|align="center" width="80"|LSL
|align="center" width="80"|Mono
|align="center" width="80"|Units
|-
|align="center"|0.07
|align="center"|0.001
|align="center"|ms/loop
|}
This test measures the overhead of a for loop that iterates an empty block just once, including its setup. The code generated for this should be something like one integer assignment, one increment, and one test-and-branch. Or as one developer said, "if it's not, it should be."
{| cellpadding="2" cellspacing="0" border="1"
|-
|align="center" width="300" rowspan="2"|state change
|align="center" width="80"|LSL
|align="center" width="80"|Mono
|align="center" width="80"|Units
|-
|align="center"|0.14
|align="center"|22.2
|align="center"|ms/transition
|}
State changes in Mono are tied to the sim frame rate of 45 fps (per Vektor Linden). The test code for state changes is an outer loop around 16 state changes like this:
. . .
state state''N''  { state_entry() { state state''N+1''; } }
state state''N+1'' { state_entry() { state state''N+2''; } }
. . . etc.
{| cellpadding="2" cellspacing="0" border="1"
|-
|align="center" width="300" rowspan="2"|v = <PI,PI,PI> + <PI,PI,PI> * PI;
|align="center" width="80"|LSL
|align="center" width="80"|Mono
|align="center" width="80"|Units
|-
|align="center"|0.43
|align="center"|0.025
|align="center"|ms/statement
|}
{| cellpadding="2" cellspacing="0" border="1"
|-
|align="center" width="300" rowspan="2"|call to llCSV2List(llList2CSV(x));
where x is a list of 10 integers
|align="center" width="80"|LSL
|align="center" width="80"|Mono
|align="center" width="80"|Units
|-
|align="center"|3.2
|align="center"|4 - 7
|align="center"|ms/call
|}
So far, the Mono results vary too much for any meaningful result.
{| cellpadding="2" cellspacing="0" border="1"
|-
|align="center" width="300" rowspan="2"|call to llDetectedName()
|align="center" width="80"|LSL
|align="center" width="80"|Mono
|align="center" width="80"|Units
|-
|align="center"|1.1
|align="center"|0.8
|align="center"|ms/call
|}
{| cellpadding="2" cellspacing="0" border="1"
|-
|align="center" width="300" rowspan="2"|call to llGetInventoryName(INVENTORY_TEXTURE, 0);
|align="center" width="80"|LSL
|align="center" width="80"|Mono
|align="center" width="80"|Units
|-
|align="center"|0.52
|align="center"|0.21
|align="center"|ms/call
|}
{| cellpadding="2" cellspacing="0" border="1"
|-
|align="center" width="300" rowspan="2"|call to llGetPos();
|align="center" width="80"|LSL
|align="center" width="80"|Mono
|align="center" width="80"|Units
|-
|align="center"|0.44
|align="center"|0.045
|align="center"|ms/call
|}
{| cellpadding="2" cellspacing="0" border="1"
|-
|align="center" width="300" rowspan="2"|call to llList2Vector(x, 0);
where x = [ ZERO_VECTOR ]
|align="center" width="80"|LSL
|align="center" width="80"|Mono
|align="center" width="80"|Units
|-
|align="center"|0.55
|align="center"|0.014
|align="center"|ms/call
|}
{| cellpadding="2" cellspacing="0" border="1"
|-
|align="center" width="300" rowspan="2"|call to llMD5String(s,0);
where s is a 64-char string
|align="center" width="80"|LSL
|align="center" width="80"|Mono
|align="center" width="80"|Units
|-
|align="center"|1.4
|align="center"|1.4
|align="center"|ms/call
|}
{| cellpadding="2" cellspacing="0" border="1"
|-
|align="center" width="300" rowspan="2"|call to llMessageLinked(LINK_THIS, 0, "", NULL_KEY);
One consumer script with empty link_message() { }
|align="center" width="80"|LSL
|align="center" width="80"|Mono
|align="center" width="80"|Units
|-
|align="center"|1.5
|align="center"|0.9
|align="center"|ms/call
|-
|align="center"|Two consumer scripts
|align="center"|1.8
|align="center"|1.2
|align="center"|ms/call
|-
|align="center"|Four consumer scripts
|align="center"|2.4
|align="center"|1.8
|align="center"|ms/call
|-
|align="center"|Eight consumer scripts
|align="center"|4.4
|align="center"|3.3
|align="center"|ms/call
|-
|align="center"|16 consumer scripts
|align="center"|9 - 20
|align="center"|7 - 15
|align="center"|ms/call
|-
|align="center"|32 - 64 consumer scripts
|align="center"|22.2
|align="center"|22.2
|align="center"|ms/call
|}
The time it takes to call llMessageLinked() is dependent on the number of consumers of the message, up to about 20-30 consumer scripts. At that point, the time to send a link message becomes constant at one call per 22.2 ms, which is suspiciously similar to the sim's basic frame rate.
{| cellpadding="2" cellspacing="0" border="1"
|-
|align="center" width="300" rowspan="2"|call to llParseString2List(s,a,b)
where s is 2000 random digits, a=["0","1"]; b=["2","3"];
|align="center" width="80"|LSL
|align="center" width="80"|Mono
|align="center" width="80"|Units
|-
|align="center"|45
|align="center"|24
|align="center"|ms/call
|}
{| cellpadding="2" cellspacing="0" border="1"
|-
|align="center" width="300" rowspan="2"|call to llSay(-2, "0123456789");
|align="center" width="80"|LSL
|align="center" width="80"|Mono
|align="center" width="80"|Units
|-
|align="center"|0.66
|align="center"|0.3 - 0.8
|align="center"|ms/call
|}
The Mono results vary too much for meaningful results.
{| cellpadding="2" cellspacing="0" border="1"
|-
|align="center" width="300" rowspan="2"|call to llSetLinkAlpha();
|align="center" width="80"|LSL
|align="center" width="80"|Mono
|align="center" width="80"|Units
|-
|align="center"|1.0
|align="center"|0.52
|align="center"|ms/call
|}
{| cellpadding="2" cellspacing="0" border="1"
|-
|align="center" width="300" rowspan="2"|call to llSetText(s,c,a)
where string s is 20 chars
|align="center" width="80"|LSL
|align="center" width="80"|Mono
|align="center" width="80"|Units
|-
|align="center"|0.79
|align="center"|0.38
|align="center"|ms/call
|}
{| cellpadding="2" cellspacing="0" border="1"
|-
|align="center" width="300" rowspan="2"|call to llSubStringIndex(s, "X"); where s is 10000 digits not containing "X"
|align="center" width="80"|LSL
|align="center" width="80"|Mono
|align="center" width="80"|Units
|-
|align="center"|16
|align="center"|22
|align="center"|ms/call
|}
=== Event handlers ===
{| cellpadding="2" cellspacing="0" border="1"
|-
|align="center" width="300" rowspan="2"|Maximum rate of dataserver() events after llGetNotecardLine()
|align="center" width="80"|LSL
|align="center" width="80"|Mono
|align="center" width="80"|Units
|-
|align="center"|6.4
|align="center"|6.4
|align="center"|events/sec
|}
{| cellpadding="2" cellspacing="0" border="1"
|-
|align="center" width="300" rowspan="2"|Maximum rate of link_message() events
|align="center" width="80"|LSL
|align="center" width="80"|Mono
|align="center" width="80"|Units
|-
|align="center"|42-45
|align="center"|42-45
|align="center"|events/sec
|}
The results were highly variable over several tens of millions of link_message() events in three lightly loaded sims, but I wonder if the maximum rate is tied to the sim frame rate of 45 fps.
{| cellpadding="2" cellspacing="0" border="1"
|-
|align="center" width="300" rowspan="2"|Maximum rate of listen() events
1 listener per script, any number of scripts per prim, any number of prims in linkset
|align="center" width="80"|LSL
|align="center" width="80"|Mono
|align="center" width="80"|Units
|-
|align="center"|14.7
|align="center"|14.7
|align="center"|events/sec
|-
|align="center"|Two listeners per script
|align="center"|11
|align="center"|11
|align="center"|events/sec
|-
|align="center"|Three listeners per script
|align="center"|8.8
|align="center"|8.9
|align="center"|events/sec
|}
{| cellpadding="2" cellspacing="0" border="1"
|-
|align="center" width="300" rowspan="2"|Maximum rate of sensor() events after llSensorRepeat()
|align="center" width="80"|LSL
|align="center" width="80"|Mono
|align="center" width="80"|Units
|-
|align="center"|15
|align="center"|22
|align="center"|events/sec
|}
The maximum rate of sensor() events seems to be independent of the sensor radius or number of things detected.
{| cellpadding="2" cellspacing="0" border="1"
|-
|align="center" width="300" rowspan="2"|Maximum rate of timer() events
Maximum rate of touch() events
|align="center" width="80"|LSL
|align="center" width="80"|Mono
|align="center" width="80"|Units
|-
|align="center"|22
|align="center"|22
|align="center"|events/sec
|}
About half the sim's basic frame rate of 45 fps.




Line 449: Line 95:
* [[User:Becky_Pippen/Memory_Efficiency|Calculating memory needed and memory usage efficiency]]
* [[User:Becky_Pippen/Memory_Efficiency|Calculating memory needed and memory usage efficiency]]
* [[User:Becky_Pippen/Script_Memory_Limits| A checklist for scripters -- techniques for reducing memory usage]]
* [[User:Becky_Pippen/Script_Memory_Limits| A checklist for scripters -- techniques for reducing memory usage]]
* [[User:Becky_Pippen/New_LSL_Functions| New LSL functions llGetLinkPrimitiveParams() and llSetLinkPrimitiveParamsFast()]]
* [[User:Becky_Pippen/New_LSL_Functions| New LSL functions llGetLinkPrimitiveParams(), llSetLinkPrimitiveParamsFast() and more]]
* [[User:Becky_Pippen/Key_Storage| Scripting techniques for storing lots of keys (UUIDs)]]
* [[User:Becky_Pippen/Hashing| Scripting techniques for hashing data to conserve memory]]
* [[User:Becky_Pippen/Numeric_Storage| Scripting techniques for storing arbitrary binary data without lists]]
* [[User:Becky_Pippen/Numeric_Storage| Scripting techniques for storing arbitrary binary data without lists]]
* [[User:Becky_Pippen/Text_Storage| Scripting techniques for compressing ASCII text in Mono]]
* [[User:Becky_Pippen/Text_Storage| Scripting techniques for compressing ASCII text in Mono]]
Line 457: Line 103:




==Shared Media-on-a-Prim LSL recipes ==
[[User:Becky_Pippen/Shared_Media_LSL_Recipes|See here]]
==Cloud Be Gone!==
Tired of being a Ruth Cloud? See [[User:Becky_Pippen/CloudBeGone|this article]] for the technical reasons why
various solutions work or not.
For fun, [[User:Becky_Pippen/RuthCloudScript|see here for a Ruth Cloud LSL script]].


== Links ==
== Links ==
* [http://slurl.com/secondlife/Nevia/128/128/23 The Community of the Nevia Archipelago] [http://www.inkwelle.com Website]
* [http://jira.secondlife.com/ JIRA Issue Tracker]
* [http://jira.secondlife.com/ JIRA Issue Tracker]
* [https://wiki.secondlife.com/wiki/About_AWG Architecture Working Group]
* [https://wiki.secondlife.com/wiki/About_AWG Architecture Working Group]
* [https://wiki.secondlife.com/wiki/Mono Mono wiki]
{{skills
{{skills
|Builder=*
|Builder=*

Latest revision as of 11:06, 2 September 2011

About

I love helping new residents get addicted... er, I mean acclimated to Second Life. Here are some notes I've picked up along the way that might be of help.

SL Glossary

Second Life's most complete glossary

What is Mono?

John and Mary

John and Mary are LSL scripters. John prefers the old-fashioned way of doing things, while Mary enjoys all the latest new technology, like Mono. They both write LSL scripts. John runs his scripts under the old way, and Mary runs hers using Mono.

When John writes an LSL script and clicks the Save button, the client viewer compiles the script into proprietary bytecode and uploads it to the servers. The server runs the script by using a proprietary interpreter to interpret the bytecode. It runs slow.

Mary writes the identical script -- no change in the LSL language syntax. When she presses "Save", the client uploads her script text to the servers where it gets compiled into standardized CIL assembly language. (CIL is a bytecode that originally came from Microsoft's .NET technology.) Because the Linden servers are Linux machines, there's no .NET framework available to run the CIL code. So the servers use the open-source Mono framework to execute the CIL. Mono includes an open-source JIT ("Just in Time") compiler that converts the CIL bytecode into machine code as it's needed, and that compiled machine code is what makes the script execute faster than the old way.

How it all Flows

Old way:

LSL  ---> LSL compiler ---> proprietary bytecode ---> LSL bytecode interpreter

New way:

LSL                        ---> LSL compiler   | 
C# (future)                ---> C# compiler    |  ---> CIL bytecode ---> Mono CIL execution engine
Other languages (future)   ---> other compiler |

Do Scripts Running in Mono Get More Memory?

Yes, it's true that each Mono script gets 64K of memory to use. Non-Mono scripts have to fit inside 16K of memory. (That's compiled code plus stack plus heap.) Unfortunately, Mono-compiled code is a bit of a memory hog and can take up to 4 times as much memory as the old runtime environment. So, scripts using Mono end up with a little more memory to play with depending on the code-to-data ratio, but not four times as much.

Evolving Terminology

LSL - Used to refer to the whole system -- the language, the front-end compiler, and the back-end interpreter. Now it's confusing because it's used in one of two ways and its meaning depends on the context: (1) LSL is the language, the source code, which has not changed with Mono. E.g., "Take this LSL script and compile it with Mono and it will run faster." Or (2) LSL refers to the old compiler front-end and interpreter virtual machine back-end. E.g., "When I compile this script with LSL it runs very slowly." Both meanings are used in the sentence, "This LSL script was compiled with LSL, but that LSL script was compiled with Mono."

The term LSO refers to the bytecode generated by the traditional LSL compiler and executed by the old LSL runtime, and is a term that can be used to distinguish the old LSL tool set from the Mono tool set.

Can We Use Other Languages?

Mono just runs CIL bytecode. It doesn't know or care what the original language was. At this time (mid-2008) only LSL source code can be compiled into CIL and run on Mono. But maybe someday we will be able to compile C# and possibly other languages into CIL which will run under Mono. A single object could contain scripts that originally were written in different languages.

The LSL language -- the source syntax -- will probably be supported for a very long time.

However, the servers will support both back-ends for a very long time -- the one that executes the old proprietary LSL bytecode, and the new Mono virtual machine that executes standard CIL. All the millions of existing LSL scripts compiled the old way will continue to run indefinitely. New LSL scripts may be compiled using the old LSL compiler or the new Mono compiler.

What To Expect After Mono Hits the Main Grid

  • Old LSL scripts already compiled: will continue to run indefinitely. You don't have to do a thing.
  • Any LSL script if you have the source code: can be recompiled under the old LSL compiler or the new Mono compiler, at your choice, for a long time to come.
  • C# and other languages: probably added at some future time.


Animation Frame Rate

The Problem

You may have heard rumors that animations (BVH files) get reduced to 12 FPS when uploaded or played, and there's no need to make an animation faster than 12 frames per second (FPS). Other rumors say that fast animation frame rates give smoother animations, while other rumors say that you should use the lowest animation frame rate possible. Confused? Here's some information that might help.

The SL Viewer Interpolates

Part of the confusion is that the term "frame rate" means different things in different contexts. The SL viewer renders frames of the scene, including any animations in the scene, as fast as it can. You can always see how fast the client is rendering frames by looking for the FPS number in the Basic section in the Statistics Bar (Ctrl-Shift-1). The frame rate in an animation file is just there to establish the time scale of the animation information. It's a different kind of "frame." So how do the two "frame rates" relate and how does it all work? It's simple -- when the viewer renders a frame, it determines how far along it is in the animation and interpolates the avatar's bone rotations based on the nearest two frames in the animation file.

Refer to the following illustration to see how you can demonstrate this interpolation. Using your favorite animation editor, make an animation file consisting of just three or four frames. The first frame is the requisite reference frame, then frame 2 is the first key frame of the animation that defines the start of a movement. Frame 3 is the last frame of the movement. Optionally you can add a frame 4 that is a copy of frame2 to make a loop. Make a large movement between frames 2 and 3, such as arms straight down in frame 2 and straight up in frame 3. Set the animation for 1 frame per second. Upload the short animation file and set the looping parameters if needed so that the animation loops continuously.

With the animation set to one frame per second, the arms should start at the sides, then one second later be raised above the head, then two seconds after the start of the animation, the arms should be back at the sides, then repeat. What happens if the client renders frames faster than one per second? If the viewer didn't interpolate, you would expect the avatar's arms to snap suddenly from one position to the other... snap up... snap down... etc. at one-second intervals. But that's not what happens at all. Regardless of your viewer frame rate, you'll see your avatar's hands moving smoothly, up and down, with all the intermediate positions between animation frames interpolated smoothly. To examine the effect in more detail, click Advanced->Character->Slow Motion Animations, then you'll have a whole bunch of interpolated rendered frames to examine.

Viewer interpolates animation frames

Asynchronous Frame Rates

In the following timeline example, the top lines represent the frames in an animation file, and the bottom lines show how fast the viewer renders frames and where the rendered frames correspond in time to the animation file. In this illustration, the animation frames are numbered starting at two, because frame one is the reference frame that never gets rendered. When the client is rendering the first frame of the animation (render frame 0 in the illustration), it corresponds to frame 2 in the animation file. In this illustration, the client is running at a fairly constant rate a little more than three times the rate of frames in the animation file: <lsl> animation frame number 2 3 4 5

animation frames: K-------------------------K-------------------------K-------------------------K-----

viewer rendered frames: |-------|-------|-------|-------|-------|-------|-------|-------|-------|-------|

viewer frame number: 0 1 2 3 4 5 6 7 8 9 10 </lsl> When the viewer is rendering frame 0, the bone rotations exactly correspond to frame 2 in the animation file. After that, the time when a frame is rendered will almost always fall somewhere between two of the animation frames and the client will interpolate the bone rotations. For example, when the client renders frame 1, the bone rotations will be something between the rotations in frames 2 and 3 of the animation file, but closer to the rotations in frame 2.

If the client is running slow in comparison to the animation file, then some data in the animation file must be skipped: <lsl> animation frame number 2 3 4 5 6 7 8 9 10 11 12 13 14 15

animation key frames: K-----K-----K-----K-----K-----K-----K-----K-----K-----K-----K-----K-----K-----K---

viewer rendered frames: |--------------|---------------|---------------|---------------|---------------|--

viewer frame number: 0 1 2 3 4 5 </lsl> In this scenario, when it's time for the client to render frame 1, the animation information is interpolated from frames 4 and 5 in the animation file, which means that any quick movement defined in frames 3 and 4 of the animation file will have been ignored that time around. If the animation loops, then perhaps on a subsequent loop the frames will align differently and a different set of animation frames will be ignored.

Other Considerations

There are additional forces at work you need to be aware of:

1. The higher the animation frame rate, the larger the file that has to be sent from the sim to every client in viewing range of the animation. Smaller is less laggy.

2. When you upload an animation, the viewer silently drops small movements. It does this by comparing each bone's rotation in two adjacent frames, then dropping any data for bones that don't move much. The exact algorithm is complicated, but in general the threshold is a few degrees in any axis from one frame to the next. If a bone's rotation changes from one frame to the next less than 3 or 4 degrees in all axes, it might be dropped. If the movement is more than about 10 degrees in any axis, it's likely to be retained. If an animation is made with a high frame rate, then the movement from frame to frame will be smaller and more movements will be dropped. If the animation frames are less frequent, then the movement between frames will be larger and more likely to be retained.

3. When the viewer interpolates bone rotations from the animation file, it uses a linear interpolation between the nearest frames, which sometimes looks a bit robotic. A higher animation rate lets you specify movements in finer resolution and make the movement start slow, get faster in the middle of the movement, then slow down when it reaches its maximum extent.

Summary

The last two forces mentioned above work against each other, so the challenge for animators is to find a balance. The animation needs to be made with a high enough frame rate to describe the movements the way you want them to appear, yet it needs to be as low a frame rate as possible to avoid having the client discard small movements on upload. It depends on the animation, but a good starting point might be around 10 FPS for the animation. Try a lower animation frame rate if the animation appears ok with fewer frames, relying more on the linear interpolation by the client to smooth it out. Use a higher animation frame rate if the linear interpolation doesn't look quite right and you need finer resolution to describe more complex or non-linear movements.


Script Memory Limits

Script Memory limits are coming in 2010! Here's some information to help you prepare;


Shared Media-on-a-Prim LSL recipes

See here

Cloud Be Gone!

Tired of being a Ruth Cloud? See this article for the technical reasons why various solutions work or not.

For fun, see here for a Ruth Cloud LSL script.

Links