Advanced compilers for Ogre's scripts

Threads related to Google Summer of Code
Post Reply
User avatar
sinbad
OGRE Retired Team Member
OGRE Retired Team Member
Posts: 19265
Joined: Sun Oct 06, 2002 11:19 pm
Location: Guernsey, Channel Islands
x 66
Contact:

Post by sinbad »

As for the extensibility aspect, I think there's 2 levels here - compile time extensibility (which is what we'll have for particle, material, fontdef etc) and runtime extensibility which might be for anything else which is not predefined. I think being able to lex anything we can predict into a tokenID is a good thing, performance wise, but keeping the ability to keep the original string-based token for other things could be beneficial for downstream uses. nfz's ScriptCompiler dealt with this by defining & registering new tokens in subclasses before pass 1 (lexing) took place and I'm sure the same thing could be done here without losing the extensibility you've added.

But, as I said before and will stress again, I'm not expecting any of my comments here to result in changes until post-SoC, because they are stylistic more than functional - just raising it as something you might want to look at later on. If we can tempt you to stay on afterwards of course :)

User avatar
Praetor
OGRE Retired Team Member
OGRE Retired Team Member
Posts: 3335
Joined: Tue Jun 21, 2005 8:26 pm
Location: Rochester, New York, US
x 3
Contact:

Post by Praetor »

I'm already poking around the parser right now, so there's no reason not add a tokenID (called wordID actually, since the token type is set to SNT_WORD). If a wordID is found in the supplied map, then it is set and the token is not stored (thus saving memory) and if no wordID is set then the token is stored. This way extensions to scripts which do not register wordIDs can still recognized the tokens when they come downstream.

I'm almost finished with the new grammar. This new grammar builds a MUCH improved AST. So improved that when i revise the compilers I just know there will be a huge boost in quality, script error tolerance, and ease in programming. When I'm all done i'll post an example of what I'm talking about here. And the wordID will be coming along for the ride.

User avatar
Praetor
OGRE Retired Team Member
OGRE Retired Team Member
Posts: 3335
Joined: Tue Jun 21, 2005 8:26 pm
Location: Rochester, New York, US
x 3
Contact:

Post by Praetor »

Wow, i was closer to done with the grammar than I expected. Here it is:

Code: Select all

material jaiqua
{
	// Hardware skinning techniique
	technique
	{
		pass
		{
			vertex_program_ref Ogre/HardwareSkinningTwoWeights
			{
			
			}
			// alternate shadow caster program
			shadow_caster_vertex_program_ref Ogre/HardwareSkinningTwoWeightsShadowCaster
			{
				param_named_auto worldMatrix3x4Array world_matrix_array_3x4
				param_named_auto viewProjectionMatrix viewproj_matrix
				param_named_auto ambient ambient_light_colour
			
			}

			texture_unit
			{
				texture blue_jaiqua.jpg
				tex_address_mode clamp
			}
		}
	}

	// Software blending technique
	technique
	{
		pass
		{
			texture_unit
			{
				texture blue_jaiqua.jpg
				tex_address_mode clamp
			}
		}
	}
	
}


material Examples/Plane/IntegratedShadows
{
	technique
	{
		pass
		{
			// Single-pass shadowing
			texture_unit
			{
				texture MtlPlat2.jpg
			}
			texture_unit
			{
				// standard modulation blend
				content_type shadow
				tex_address_mode clamp
			}
		}
	}
	
}
turn into this:

Code: Select all

type: 2 isProperty: false isObject: true wordID: 3452816845 token: material
	 type: 2 isProperty: false isObject: false wordID: 0 token: jaiqua
	 type: 6 isProperty: false isObject: false wordID: 3452816845 token: 
		 type: 2 isProperty: false isObject: true wordID: 3452816845 token: technique
			 type: 6 isProperty: false isObject: false wordID: 3452816845 token: 
				 type: 2 isProperty: false isObject: true wordID: 3452816845 token: pass
					 type: 6 isProperty: false isObject: false wordID: 3452816845 token: 
						 type: 2 isProperty: false isObject: true wordID: 3452816845 token: vertex_program_ref
							 type: 2 isProperty: false isObject: false wordID: 0 token: Ogre/HardwareSkinningTwoWeights
							 type: 6 isProperty: false isObject: false wordID: 3452816845 token: 
							 type: 7 isProperty: false isObject: false wordID: 3452816845 token: 
						 type: 2 isProperty: false isObject: true wordID: 3452816845 token: shadow_caster_vertex_program_ref
							 type: 2 isProperty: false isObject: false wordID: 0 token: Ogre/HardwareSkinningTwoWeightsShadowCaster
							 type: 6 isProperty: false isObject: false wordID: 3452816845 token: 
								 type: 2 isProperty: true isObject: false wordID: 0 token: param_named_auto
									 type: 2 isProperty: false isObject: false wordID: 0 token: worldMatrix3x4Array
									 type: 2 isProperty: false isObject: false wordID: 0 token: world_matrix_array_3x4
								 type: 2 isProperty: true isObject: false wordID: 0 token: param_named_auto
									 type: 2 isProperty: false isObject: false wordID: 0 token: viewProjectionMatrix
									 type: 2 isProperty: false isObject: false wordID: 0 token: viewproj_matrix
								 type: 2 isProperty: true isObject: false wordID: 0 token: param_named_auto
									 type: 2 isProperty: false isObject: false wordID: 0 token: ambient
									 type: 2 isProperty: false isObject: false wordID: 0 token: ambient_light_colour
							 type: 7 isProperty: false isObject: false wordID: 3452816845 token: 
						 type: 2 isProperty: false isObject: true wordID: 3452816845 token: texture_unit
							 type: 6 isProperty: false isObject: false wordID: 3452816845 token: 
								 type: 2 isProperty: true isObject: false wordID: 0 token: texture
									 type: 2 isProperty: false isObject: false wordID: 0 token: blue_jaiqua.jpg
								 type: 2 isProperty: true isObject: false wordID: 0 token: tex_address_mode
									 type: 2 isProperty: false isObject: false wordID: 0 token: clamp
							 type: 7 isProperty: false isObject: false wordID: 3452816845 token: 
					 type: 7 isProperty: false isObject: false wordID: 3452816845 token: 
			 type: 7 isProperty: false isObject: false wordID: 3452816845 token: 
		 type: 2 isProperty: false isObject: true wordID: 3452816845 token: technique
			 type: 6 isProperty: false isObject: false wordID: 3452816845 token: 
				 type: 2 isProperty: false isObject: true wordID: 3452816845 token: pass
					 type: 6 isProperty: false isObject: false wordID: 3452816845 token: 
						 type: 2 isProperty: false isObject: true wordID: 3452816845 token: texture_unit
							 type: 6 isProperty: false isObject: false wordID: 3452816845 token: 
								 type: 2 isProperty: true isObject: false wordID: 0 token: texture
									 type: 2 isProperty: false isObject: false wordID: 0 token: blue_jaiqua.jpg
								 type: 2 isProperty: true isObject: false wordID: 0 token: tex_address_mode
									 type: 2 isProperty: false isObject: false wordID: 0 token: clamp
							 type: 7 isProperty: false isObject: false wordID: 3452816845 token: 
					 type: 7 isProperty: false isObject: false wordID: 3452816845 token: 
			 type: 7 isProperty: false isObject: false wordID: 3452816845 token: 
	 type: 7 isProperty: false isObject: false wordID: 3452816845 token:
What you see here is finally a very nested and non-flat AST. The current grammar, the one in CVS HEAD now, would not have anywhere near the detail of this structure. Notice how information like how many values are related to each property is embedded in the structure of the AST itself. Some tokens already aren't saved (like '{' '}' ':', etc.) since they have token type values to identify them. I haven't yet put in the wordID map system, but you'll notice there is a variable for it waiting. This kind of AST is going to make things a whole lot easier. The grammar itself is spread out over several sub-grammars, so it isn't easy to just post. You can check it out in the code if you'd like.

User avatar
Praetor
OGRE Retired Team Member
OGRE Retired Team Member
Posts: 3335
Joined: Tue Jun 21, 2005 8:26 pm
Location: Rochester, New York, US
x 3
Contact:

Post by Praetor »

To toot my own horn only a little, I did quite a bit in this weekend jam session. The grammar is performing beautifully now, and I just committed the updated ParticleScriptCompiler. This time, the new compiler is integrated into the ParticleSystemManager and if you enable it (OGRE_USE_NEW_COMPILERS=1) then it will be used to compile all your particle scripts. To complete this integration I just have to add an accessor function to ParticleSystemManager so you can get at its compiler instance, to add listeners and such.

My next step is to tackle the material compiler. Again. This time I'm going to start by putting in the wordID system. The particle compiler doesn't use it, so I didn't need to put it in. With that in place material compilation should go much easier than last time.

User avatar
Praetor
OGRE Retired Team Member
OGRE Retired Team Member
Posts: 3335
Joined: Tue Jun 21, 2005 8:26 pm
Location: Rochester, New York, US
x 3
Contact:

Post by Praetor »

Ouch need some assistance. Compilers appear to be things that are traditionally thread-local. Here's the case: we have background threading on, so when a particle script compiles, it does so in a separate thread. The compiler it uses is specific to this background. Now the main application wants to set up a listener to override some behavior. Obviously you won't be obtaining a reference to the compiler from the background thread. So the question is, how do I allow someone to access the compiler that will actually be used to compiler scripts so they can add listeners and such?

My only solution would be to store a collection of listeners in the ParticleSystemManager itself. Then, when a new ParticleScriptCompiler is created for a specific thread during the parseScript call, the first thing that happens is those listeners are registered. Calls to add/removeListener and parseScript would need to be synchronized, but I think that would work. What do you think?

User avatar
tuan kuranes
OGRE Retired Moderator
OGRE Retired Moderator
Posts: 2653
Joined: Wed Sep 24, 2003 8:07 am
Location: Haute Garonne, France
x 4
Contact:

Post by tuan kuranes »

You solution will sure be user threadable, meaning use can handle the synchronized on its own thread, not necessarly on the 'main ogre thread'. Then that seems ok to me.

Are you sure there's no case where users listeners registered methods won't need to write/read variables from "other threads". (ie: if user just want to make it's own customization, but just "const static method"). Perhaps giving a way to avoid blocking calls there would be interesting. (Threaded parse and "LockFreeListener").

So perhaps both solution can coexists.

User avatar
sinbad
OGRE Retired Team Member
OGRE Retired Team Member
Posts: 19265
Joined: Sun Oct 06, 2002 11:19 pm
Location: Guernsey, Channel Islands
x 66
Contact:

Post by sinbad »

The way we deal with this in the resource system is by assuming that most users won't want the complication of getting a callback in a separate thread, so we queue the notifications to be processed by the main thread. It is possible to get callbacks in the background thead too but we put a big health warning on it explaining that this is a direct callback that can in a different thread, and the receiver better know what the hell they're doing.

In your case deferring the call may not be an option. Would the callbacks have the same meaning / functionality if they happened later rather than during loading? If not, then they'll have to be in the thread itself, in which case you should put the same kinds of warnings on it about threading as we do where applicable.

As for where to store the listeners, I also think this means the listeners have to be separate from the compilers. The compilers are sort of the 'worker bees' rather than the 'queen' when it comes to the process of managing script compilation so they probably shouldn't hold the listeners. Your choice then becomes whether to put it on the corresponding manager, or somewhere even more general like ResourceGroupManager, where ScriptLoaders are registered anyway.

User avatar
Praetor
OGRE Retired Team Member
OGRE Retired Team Member
Posts: 3335
Joined: Tue Jun 21, 2005 8:26 pm
Location: Rochester, New York, US
x 3
Contact:

Post by Praetor »

I've put them in the manager for now. I'll add a warning. The main use of the listeners is to override compiler behavior, not just receive notifications, so they must be called in a blocking fashion from the compiler itself. The synchronization is as light as possible. The call to set the listener only blocks the subsequent call to feed that listener into the compiler upon calling parseScript, not the actual compilation itself. Like this:

Code: Select all

void ParticleSystemManager::setCompilerListener(ParticleScriptCompilerListener *listener)
{
	OGRE_LOCK_AUTO_MUTEX
	mCompilerListener = listener;
}

void ParticleSystemManager::parseScript(DataStreamPtr& stream, const String& groupName)
{
    ...
    {
         OGRE_LOCK_AUTO_MUTEX
         mScriptCompiler->setListener(mCompilerListener);
    }
    mScriptCompiler->compile(stream, groupName);
    ...
}

User avatar
Praetor
OGRE Retired Team Member
OGRE Retired Team Member
Posts: 3335
Joined: Tue Jun 21, 2005 8:26 pm
Location: Rochester, New York, US
x 3
Contact:

Post by Praetor »

I've got another hiccup. Not an issue, just a little thing. In Example.material there are material like this:

Code: Select all

material 2 - Default
{
    ...
}
and

Code: Select all

material Material #8
{
    ...
}
Now, I can handle these sorts of names for materials, but just pushing tokens together to form the whole name, but it seems better for all involved to do this:

Code: Select all

material "2 - Default"{...}
Quotes like this can be handled automatically for names in my grammar. Obviously for now I need to support the old scripts, fine. But somehow putting quotes around identifiers with spaces seems so much more natural.

User avatar
Praetor
OGRE Retired Team Member
OGRE Retired Team Member
Posts: 3335
Joined: Tue Jun 21, 2005 8:26 pm
Location: Rochester, New York, US
x 3
Contact:

Post by Praetor »

On second thought, those spaced identifiers could cause a few problems, since some contain isolated numbers. Number are treated specially when on their own. Quotes would really go a long way here.

User avatar
Praetor
OGRE Retired Team Member
OGRE Retired Team Member
Posts: 3335
Joined: Tue Jun 21, 2005 8:26 pm
Location: Rochester, New York, US
x 3
Contact:

Post by Praetor »

Two weeks until the end....

I've got half of the texture_unit options finished. After that, the gpu programs. Then the material compiler is done. The good news is that some of the demos already run (well the ones that don't use gpu programs). They start up slowly (each demo loads ALL the demo resources, kind of wasteful). I think optimization is going to need to become a focus after August. I want to plow through the rest of the material compiler stuff. Test it on all current scripts. I may spend 1 day and write a simple demo with a few scripts incorporating the new features, but it probably won't be too expansive. I still want to finish the compositor compiler.

Going into September (I have a really light course load this year) I'll be looking to optimize and document the compilers so that they can go into Shoggoth. There's been some movement on the "effects" front like CgFX, and a proposed effects system for GL and I think these compilers will keep Ogre ahead of the game.

User avatar
sinbad
OGRE Retired Team Member
OGRE Retired Team Member
Posts: 19265
Joined: Sun Oct 06, 2002 11:19 pm
Location: Guernsey, Channel Islands
x 66
Contact:

Post by sinbad »

Cool, we'll definitely welcome your continued involvement, there will definitely be plenty of compatibility testing and profiling to be done in the coming weeks.

GSoC officially 'finishes' on Monday (in terms of the code that is to be reviewed) but I certainly hope that's not the end of the projects / Ogre involvement for most students. Obviously your involvement predated GSoC anyway... So yeah, please do blast on through as if the finish line doesn't exist :)

You're right that it would be a good idea to encourage the quoting of identifiers when they have spaces in them now. Quoted identifiers should never be internally parsed. I would request that to deal with backwards compatibility it would be nice to have the parser just amalgamate anything between 'material' and the opening brace as the name, which I think is what you suggested.

User avatar
Praetor
OGRE Retired Team Member
OGRE Retired Team Member
Posts: 3335
Joined: Tue Jun 21, 2005 8:26 pm
Location: Rochester, New York, US
x 3
Contact:

Post by Praetor »

I'll just let you know about the problems with spaced ids real quick. The ones that are in Example.material contain numbers and words. The problem is that the grammar does not store the strings for numbers, it automatically converts them to Ogre::Reals at parse time. So, when it comes time to get that id back out, it comes in the float form (2.000000 instead of 2) and the name is not what you expected. Not sure how to get around this except to start storing string tokens for numbers. But I stopped doing that specifically because I was hoping it wouldn't be needed (saved some memory). Perhaps not.

The nice thing about quoted strings is that they are not internally parsed. Meaning they maintain any newlines and tabs within themselves. The one thing I'm currently sour about is that it does not support escaped quotes, so you can't embed a quote inside a quote like this:

Code: Select all

"this \"is a test string\""
That production should be fairly natural to most coders. Not supported yet in the grammar. If someone can step up and throw down an EBNF grammar that does support it I can pretty easily throw it into spirit and we will be all set there. Perhaps I need to employ a little look-ahead production for this, though look-aheads really kill runtime performance.

And whoops, I guess I didn't read the timeline well enough. I'll have to look up this uploading of code for tomorrow. Oh well, I have 3 weeks before my classes even start.

User avatar
volca
Gnome
Posts: 393
Joined: Thu Dec 08, 2005 9:57 pm
x 1
Contact:

Post by volca »

Great work! I'm really looking forward to this. Thanks for it!

I've got one small question though. You say that the numbers encountered are automatically converted to Reals. Isn't this a bit of a drawback? I may be using the current Compiler2Pass in a special way, but I often use integers - for example size specifications or version numbers in a number form ('integer' or 'integer.integer'), and converting to Real would render those values useless.

In my humble opinion, it would not be such a great waste to store the string value as well, to let people access the original number without the precision lost. This would be useful for the unquoted labels as well, as you say...
Image

User avatar
Praetor
OGRE Retired Team Member
OGRE Retired Team Member
Posts: 3335
Joined: Tue Jun 21, 2005 8:26 pm
Location: Rochester, New York, US
x 3
Contact:

Post by Praetor »

I'm not sure you would in practice lose anything. The integer 2 is stored as 2.0000... in a floating-point value. The compiler will automatically convert this value to an integer. I do the conversion to Reals at parse-time to help ensure it happens only once, since those sorts of string conversions can be slow. I didn't want to store both a Real and an integer for a few reasons.

For the sake of backwards compatibility I am now storing the string token for numbers in object headers only. Properties still only store the actual number value.

Are you saying to base logic in your program on whether or not a integer or a floating value is specified?

User avatar
volca
Gnome
Posts: 393
Joined: Thu Dec 08, 2005 9:57 pm
x 1
Contact:

Post by volca »

Thanks for a fast answer!
Praetor wrote:Are you saying to base logic in your program on whether or not a integer or a floating value is specified?
I'm not sure if I understand you correctly. In current implementation, I get the String version of the token and do StringConverter::parseLong(str). So it would seem to me that - yes, I specifically expect a long integer to be encountered.

The usage of those integers is for (for example) array length definition. I would fear that, as floats are never 100% precise, it could in theory happen that the array specified would be implemeted shorter/longer (rounding error). Maybe this is just a groundless fear.

Another usage is for versioning of some data structures, where major and minor numbers are stored as separate binary 32-bit numbers.

I understand that the integer version of the token is not needed in Ogre's standard scripts, is that correct?
Image

User avatar
Praetor
OGRE Retired Team Member
OGRE Retired Team Member
Posts: 3335
Joined: Tue Jun 21, 2005 8:26 pm
Location: Rochester, New York, US
x 3
Contact:

Post by Praetor »

Well, storing the string tokens for all numbers as well as the parsed Real would be one solution. I was attempted to squeeze out some memory optimization my not storing tokens unless absolutely necessary. This is, of course, without examining the compilers' memory usage yet, so I don't even know if such measures are needed. I will definitely keep it in mind when I do start the optimization process.

CABAListic
OGRE Retired Team Member
OGRE Retired Team Member
Posts: 2903
Joined: Thu Jan 18, 2007 2:48 pm
x 57
Contact:

Post by CABAListic »

I would fear that, as floats are never 100% precise, it could in theory happen that the array specified would be implemeted shorter/longer (rounding error). Maybe this is just a groundless fear.
Actually, as far as I recall the operation of floating point numbers, a float *can* store integer numbers of up to 2^23 precisely, with no rounding errors. That's because it uses 1 bit for sign, 23 bit for the mantissa and 8 bits for the exponent. With an exponent of 0 that leaves 2^23 exact integer representations, positive or negative.

User avatar
volca
Gnome
Posts: 393
Joined: Thu Dec 08, 2005 9:57 pm
x 1
Contact:

Post by volca »

Praetor wrote:Well, storing the string tokens for all numbers as well as the parsed Real would be one solution. I was attempted to squeeze out some memory optimization my not storing tokens unless absolutely necessary. This is, of course, without examining the compilers' memory usage yet, so I don't even know if such measures are needed. I will definitely keep it in mind when I do start the optimization process.
Thanks for that option!
CABAListic wrote:Actually, as far as I recall the operation of floating point numbers, a float *can* store integer numbers of up to 2^23 precisely, with no rounding errors. That's because it uses 1 bit for sign, 23 bit for the mantissa and 8 bits for the exponent. With an exponent of 0 that leaves 2^23 exact integer representations, positive or negative.
You're probably right at this. I was trying to remember the mantissa size. I also could not remember if the float numbers are stored normalized or not. And how this normalization works. 1000 and 1000.12 can both be expressed, and should probably have the same exponent. I'm sorry I do not know more about this.

In my opinion, if the compiler should be universal, an option to extract the original token form would be nice, as no-one knows if this small thing would limit the usage in some way... For example, I also use hexadecimally coded numbers next to decimally coded as an alternative, simplifying flags reading. This would confuse the float parser probably.
Image

User avatar
Praetor
OGRE Retired Team Member
OGRE Retired Team Member
Posts: 3335
Joined: Tue Jun 21, 2005 8:26 pm
Location: Rochester, New York, US
x 3
Contact:

Post by Praetor »

The grammar for numbers recognizes number literals in the form used in C/C++. Base-10 numbers that is. In C++ you proceed hex with "0x", in which case the number parser in the compiler would recognize that as just another token and store it for you. You would then have to do your own conversion later, which I imagine is perfectly fine for you.

User avatar
Praetor
OGRE Retired Team Member
OGRE Retired Team Member
Posts: 3335
Joined: Tue Jun 21, 2005 8:26 pm
Location: Rochester, New York, US
x 3
Contact:

Post by Praetor »

As an update for today (list style for easier reading):

- Fixed a bug dealing with optional parameters in the "texture" property
- The string tokens for numbers are stored in the AST
- For backwards compatibility, anything between "material" and "{" is used as the material name. For now this is only used for material naming. Naming techniques, particle systems and such with spaces should use enclosing quotes.
- Finished implementing all texture_unit properties

I've been testing with the Demo_ParticleFX since it tests the material and particle compilers at once. I'm now testing with more of the demos, and then I'll test with my own custom demo. This, I suppose, ends the official work for SoC. I'll see you all in sudden-death overtime!

User avatar
volca
Gnome
Posts: 393
Joined: Thu Dec 08, 2005 9:57 pm
x 1
Contact:

Post by volca »

Praetor wrote:- The string tokens for numbers are stored in the AST
This, I suppose, ends the official work for SoC. I'll see you all in sudden-death overtime!
Thank you for that quick fix, and congratulations in advance for finishing this project!
Image

User avatar
Kencho
OGRE Retired Moderator
OGRE Retired Moderator
Posts: 4011
Joined: Fri Sep 19, 2003 6:28 pm
Location: Burgos, Spain
x 2
Contact:

Post by Kencho »

Praetor wrote:That production should be fairly natural to most coders. Not supported yet in the grammar. If someone can step up and throw down an EBNF grammar that does support it I can pretty easily throw it into spirit and we will be all set there. Perhaps I need to employ a little look-ahead production for this, though look-aheads really kill runtime performance.
I'll see what I can figure out with a little thought (don't have the time now :()

Seems things are going fairly well for you :) Just a little stretch now and you'll be done for this summer. Keep it up!
Image

User avatar
Praetor
OGRE Retired Team Member
OGRE Retired Team Member
Posts: 3335
Joined: Tue Jun 21, 2005 8:26 pm
Location: Rochester, New York, US
x 3
Contact:

Post by Praetor »

I'm so close to wrapping up this material compiler. I've just one snag that won't go away. Luckily I no longer have to go it alone. The latest code is checked in the and you can see the behavior by running the shadows demo. I will post screenshots to give you an idea what is happening tomorrow. I just can't seem to figure it out. It appears as though textures are not being fed properly into gpu programs. The visual effect changes depending on the shadowing technique used, but it always looks like a texturing issue. I'll keep stepping through, but hopefully tomorrow the screenshots will give people clue so they can help.

User avatar
Praetor
OGRE Retired Team Member
OGRE Retired Team Member
Posts: 3335
Joined: Tue Jun 21, 2005 8:26 pm
Location: Rochester, New York, US
x 3
Contact:

Post by Praetor »

Here's some screenshots. First is stencil shadows. Something happens to our normal mapping...

Image

Standard texture shadows now and everything appears to work.

Image

Finally, some depth shadow mapping.

Image

Something is barely off with the first, but the last one appears to completely mess up the feeding of textures into the shaders (except the shadowmap textures themselves, since shadowing seems to still work).

Post Reply