Java-Gaming.org Hi !
Featured games (83)
games approved by the League of Dukes
Games in Showcase (522)
Games in Android Showcase (127)
games submitted by our members
Games in WIP (590)
games currently in development
News: Read the Java Gaming Resources, or peek at the official Java tutorials
 
    Home     Help   Search   Login   Register   
Pages: [1]
  ignore  |  Print  
  JStella - Atari 2600 emulator in Java - performance issues  (Read 5060 times)
0 Members and 1 Guest are viewing this topic.
Offline Mauvila

Senior Newbie





« Posted 2007-09-12 22:41:26 »

I manage the JStella project at SourceForge (at http://jstella.sourceforge.net).  JStella is an Atari 2600 emulator written in Java.  It is based on the open source Stella software, which is written in C++.  I translated Stella into Java mainly to prove wrong the people who said that it would not work well in Java.  And I think I have been largely successful in this...it currently runs just as well as the C++ on my computer.  But I really don't think it's optimized...I'm not a Java2D expert, or a Java performance person, and I think there is a lot of stuff relating to graphics in the code that causes it to not be as fast as it should.  (For example, I just found out about the createCompatibleImage method...that improved performance on my machine dramatically, but apparently not so much on other machines.)

I use clipping, so the main problems are when things on different parts of the screen get changed.  There is some slowdown...it slows down the virtual CPU (of the Atari) as well, which seems to me like somehow the code in the "calculation" thread is blocking while waiting for the thread that does the painting to finish...is this normal?  Of course, I may be completely wrong.  But if any Java2D pros out there want to look at it, contribute, etc., I (et al.) would be grateful.  (You can go to the JStella project page on Sourceforge and check out the CVS repository, where all the most recent source code is...the downloadable source code is a few weeks old.)
JLA
Offline Linuxhippy

Senior Devvie


Medals: 1


Java games rock!


« Reply #1 - Posted 2007-09-13 20:01:42 »

Hi,

I did some profiling, here are the results. I hope they  will be useful for you - I don't have the time to get everything up&running:

1.) Since the software your emulator runs seem to render directly to virtual "RAM" there is no way to get hardware accaleration.
You set the pixels of your backBuffer using:
1  
 yBackBuffer.setRGB(x, y, zNewPaintedColor); 

This is very sub-optimal if a large quantity of pixels has to be set (as in your case).

As better way is to grab the pixel-array directly:
1  
 byte[] data=((DataBufferByte)tex.getRaster().getDataBuffer()).getData()

You can do this once at backbuffer-initialization.
This usually detroys hw-accaleration, but because its not possible at all in your case don't worry ^^

2.) A lot of time is lost in your paint-method when you paint the backBuffer to "real" hardware:
1  
2  
3  
4  
  public void paint(Graphics g) {
            //super.paint(g);
            Graphics2D z2D=(Graphics2D)g;
           if (myImage!=null) z2D.drawImage(myImage, myTransform, null);

Could you try to just draw the parts of the image you really need - using another drawImage-Method.
I am not sure about this, maybe Java2D misses some optimizations, maybe not.

Hope that helps, Good Luck!

lg Clemens
Offline Mauvila

Senior Newbie





« Reply #2 - Posted 2007-09-15 08:30:09 »

Thanks for looking at the code.   

I'm not familiar with how Java does hardware/software rendering...as it is now, it checks to see if a pixel in a virtual buffer is different than the new one, and only if it is does it call setRGB.  So this means that some frames will do this for every pixel, while others will only do it for 10 or so...would the DataBuffer technique be faster (or equal) in both cases?  And should the emulator do hardware acceleration?  Everything about the emulator is flexible...well, except for the caveat (my own preference really) that it be pure Java.

As far as the painting of the back buffer goes, it does use clipping/dirty-rect technique, but does so through the repaint command...
e.g.
1  
repaint(rectangleThatNeedsToBeRepainted);
  I assume this works as I think it does, because when there is a busy frame (with different parts of the frame needing repainting), it takes a lot longer to paint.

And while I'm at it, what do you know about Toolkit.sync()?  Someone seemed to suggest it was necessary for Linux animations.  I think the current  version uses it, but I don't know if it slows stuff down, is unnecessary, etc...

By the way, if you want to check out the performance of the JStella applet, there is currently an applet set up at http://www.ataritimes.com/jstella/index.php.

Thanks again
JLA


Games published by our own members! Check 'em out!
Legends of Yore - The Casual Retro Roguelike
Offline Linuxhippy

Senior Devvie


Medals: 1


Java games rock!


« Reply #3 - Posted 2007-09-17 18:04:24 »

Hi again,

Quote
I'm not familiar with how Java does hardware/software rendering...as it is now, it checks to see if a pixel in a virtual buffer is different than the new one, and only if it is does it call setRGB.  So this means that some frames will do this for every pixel, while others will only do it for 10 or so...would the DataBuffer technique be faster (or equal) in both cases?
I would not waste too much time tinkering arround with this, simply use the DataBuffer approach (you'll get a int[] where the colors are masked) and try it out.
Because you use clipping it should be faster in almost all cases :-)

Quote
  And should the emulator do hardware acceleration?  Everything about the emulator is flexible...well, except for the caveat (my own preference really) that it be pure Java.
Well this is quite harder to archive. "Hardware accaleration" in this context means that the emulator sends calls like drawLine() or fillRect() to java, java passes those commands to the OS, the OS to the driver and the driver to the card. However this has only benefits if the commands touch larger areas, for 1-20pixel its slower because of the additional overhead. I guess generating such call-sequences efficiently from your emulator isn't possible at all easily.
However this shouldn't be a real problem, as far as I understand rendering is done by the emulated application itself, so your emulator does only convert index'ed colors to rgb and paint the image.

Quote
As far as the painting of the back buffer goes, it does use clipping/dirty-rect technique, but does so through the repaint command...
e.g.
1  
repaint(rectangleThatNeedsToBeRepainted);
  I assume this works as I think it does, because when there is a busy frame (with different parts of the frame needing repainting), it takes a lot longer to paint.
Well good to know. What you see is the process of migrating the image from RAM to video-ram. You most likely won't be able to get this away - but I am quite sure the databuffer-approach will do way better.

Quote
And while I'm at it, what do you know about Toolkit.sync()?  Someone seemed to suggest it was necessary for Linux animations.  I think the current  version uses it, but I don't know if it slows stuff down, is unnecessary, etc...
Well its does basically a sync of all buffered stuff. If it does not hurt let it be there.

Good luck, lg Clemens
Offline Mauvila

Senior Newbie





« Reply #4 - Posted 2007-09-18 00:06:46 »

Thanks again for your help.
As far as the getDataBuffer() stuff goes, what do I do if I create my BufferedImages via createCompatibleImage method in the toolkit?  I really have no way of knowing what the data type of the image is going to be (byte, int)...although I'm pretty sure my system generates ints.  Would I have to forgo the createCompatibleImage() method?

Thanks
JLA
Offline CommanderKeith
« Reply #5 - Posted 2007-09-18 02:51:29 »


There's a good explanation of toolkit.sync() here: http://www.java-gaming.org/forums/index.php?topic=15000.0

Offline Linuxhippy

Senior Devvie


Medals: 1


Java games rock!


« Reply #6 - Posted 2007-09-18 08:19:57 »

As far as the getDataBuffer() stuff goes, what do I do if I create my BufferedImages via createCompatibleImage method in the toolkit?  I really have no way of knowing what the data type of the image is going to be (byte, int)...although I'm pretty sure my system generates ints.  Would I have to forgo the createCompatibleImage() method?
Yes, use INT_RGB as data-type. Why don't you simply try it and post the results?

lg Clemens
Offline Linuxhippy

Senior Devvie


Medals: 1


Java games rock!


« Reply #7 - Posted 2007-09-20 13:14:03 »

and, what experiences did you make? I am quite interested in your results Smiley
Offline Mauvila

Senior Newbie





« Reply #8 - Posted 2007-09-21 01:30:31 »

I implemented something with getCompatibleImage thing that says if the type is an integer type with the equivalent RGB format, to use the method you suggested.  For those that get a non-equivalent type of image, it uses the old way of doing it.  I can't see a change on my system, but I never had a problem before, so there isn't much to see.  But from profiling using the nanoTIme feature, it appeared to me that the drawing to the back buffer wasn't necessarily the bottle neck, but instead it appeared that the bottle neck was the calculations part, but I think this was because it blocking due to the part that paints the back buffer to the screen...but I have no idea why it would block, because they are in separate threads.  This bottleneck and delay cleared up on my system when I originally did the getCompatibleImage thing, but it apparently hasn't on some other systems.  But this strongly suggested to me that the problem is in the graphics/threads.

I haven't been able to spend much time on it because of medical school, but if you (or whoever) want to become a developer on JStella, you would be more than welcome.  And you could experiment around with it.  There is a sort of fan-base for the software over at AtariAge.com, but they are more arcane assembly language than Java over there, so that's why I came here.   

JStella has had about 600 downloads over the past 8 or so weeks since its introduction. (This doesn't include people who play the JStella applet on websites) . The parent program Stella (in C++) averages about that many downloads in a day.  So a goal for anyone interested in promoting Java for desktop applications etc. (and helping prove Stevie Jobs wrong) would be for JStella to become competitive with its parent in popularity.  Let me know if you or anyone here is interested in becoming a developer on this SourceForge project...
Thanks again
JLA
Offline Mauvila

Senior Newbie





« Reply #9 - Posted 2007-09-24 00:32:40 »

Whoops...hadn't updated the CVS when I made that last post...now it is updated.  I hope it doesn't mess things up...I am assuming that TYPE_INT_RGB, TYPE_INT_ARGB, and TYPE_INT_ARGB_(forgot what was here) are all the same general format, in terms of what color byte is located where.  If not, then some users may not be able to use the program...
JLA
Games published by our own members! Check 'em out!
Legends of Yore - The Casual Retro Roguelike
Offline Linuxhippy

Senior Devvie


Medals: 1


Java games rock!


« Reply #10 - Posted 2007-09-24 10:13:27 »

Hi again,

I maybe know why you could experience blocking between the CPU and the Java2d-rendering thread.
As far as I understand you do the setRGB from one thread (is this also the thread the virtual CPU runs on?), whereas you render the image with drawImage on another thread.
Well as long as the image is rendered (which will take quite a lot of time, because it needs to be scaled and a vram upload has to be done), you can't call setRGB of course because Java2D synchronizes on the image to avoid corruption.

1.) If its really the case that one thread may render the image you are actually painting to, I would create two images, one which is currently drawn to and one which is rendered.

2.) Please don't overrate the compatibleImage stuff. You do operations with the image where it almost makes no difference wether your image has a compatible format of not - it is directed to the software rendering loops anyway. Can can read almost everywhere that setRGB is not recommended when setting many pixels (and because you are clipping anyway, it does not have an advantage for you if only a few pixels are changes).

3.) Don't forget to synchronize when accessing the buffer of the image wher you are in another thread than the painting thread, like:
[source]
synchronized(image)
{
  int[] raster = image.getRaster ...
  raster[i+x*y] = ......
}
[/source]
All in all I did not see many synchronization-statements in your code, I don't your code well so it could be that everything is fine but since you use more than one thread - have you thought about potential multi-threading problems?

lg Clemens
Offline Mauvila

Senior Newbie





« Reply #11 - Posted 2007-09-24 17:25:48 »

So you suggest to just manually use TYPE_INT_RGB?  (This is what is returned as a "compatible image" with my setup.)

I used to have a lot of stuff synchronized, but I started eliminating them because I felt many of them were unneeded.  Every once in a while I'll get an exception thrown due to threads, but it isn't that often...I know thread handling needs to be tightened, but I figure I need to find where the blocking is going on first. 

I originally thought that the setRGB was blocking, but upon manual profiling, the delay seemed to be in a previous method call (in the same thread--the processFrame() method in JSTIA, called from JSConsole doFrame() method).  Maybe I'm totally wrong about the thread thing, but when the screen is large (i.e. the stand-alone window maximized), the paint-to-the-screen method would often take around 54 milliseconds (very long), and the processFrame() part in a different thread that immediately followed (well, actually called concurrently) would take the same amount, which is WAY too long...and when I did the createCompatibleImage thing, neither of them would last that long and so the problem was fixed for MY setup.   The processFrame() is just calculation stuff and shouldn't be dependent on the size of the emulator window or the rendering methods.  Or should it...?

Thanks
JLA
Offline Mauvila

Senior Newbie





« Reply #12 - Posted 2007-09-25 00:56:23 »

I just got word from one of the people who still had the slowdown, and he says that the newest version (0.8 - implementing the aforementioned changes) no longer slows down...that's good news.  Of course, there is still a lot of optimization that needs to be done, but I think one of the big hurdles has been jumped.  Thanks for your help.
JLA
Offline Linuxhippy

Senior Devvie


Medals: 1


Java games rock!


« Reply #13 - Posted 2007-09-25 12:32:17 »

Hi again,

Quote
I just got word from one of the people who still had the slowdown, and he says that the newest version (0.8 - implementing the aforementioned changes) no longer slows down...that's good news.
Good to hear, glad that my hints have helped Smiley

Quote
So you suggest to just manually use TYPE_INT_RGB?  (This is what is returned as a "compatible image" with my setup.)
Yes. Using an image format different to what getCompatibleImage returns will not make a difference for you, because your operations cannot be done in hardware anyway. I really would replace the setRGB/compatible image path and always create an INT_RGB and access the raster-data directly. Trust me Wink

Quote
I used to have a lot of stuff synchronized, but I started eliminating them because I felt many of them were unneeded.  Every once in a while I'll get an exception thrown due to threads, but it isn't that often...I know thread handling needs to be tightened, but I figure I need to find where the blocking is going on first.
Well operating on not-synchronized , thread-shared data with multiple threads is really dangerous. On some systems it may work all the time, on some it may work with some exceptions and on some it won't even run. The problem is that I can't give you a small hint what to do and it will work as with the slowdowns - it really depends. Under some circumstances clever tricks are needed to do as little synchronization as possible - because it tends to be a quite expensive operation under some cirumstances.

To be honest I don't have any spare time, but I'll have a look to fix the most problematic parts.

lg Clemens
Offline Linuxhippy

Senior Devvie


Medals: 1


Java games rock!


« Reply #14 - Posted 2007-09-25 13:31:49 »

I went a bit through your code and it seems that its harder to fix the concurrency-problems than I thought.

If you're interested in the topic I really can recommend the following article: http://www.ibm.com/developerworks/java/library/j-threads1.html
Its a series of 3 aticles about threading with Java, and although its quite old (2001) it explains all things that are important to know.
It mentions the dangers, how and when synchronization is slow and so on....
Offline Linuxhippy

Senior Devvie


Medals: 1


Java games rock!


« Reply #15 - Posted 2007-09-25 13:53:47 »

Just a few more suggestions:

1.) In the CPU-Thread, which starts the painting only access a int[]. This is a seperate copy of the image buffer, not the raster of the BufferedImage.
Don't touch the BufferedImage itself in the CPU-Thread, better do this in the event-thread in paint()

Every time you access the local int[] synchronize it, but not in a loop, e.g.
synchronized(localBuffer)
{
  for(int i=0; i < localbuffer.lenght ....
}

In the event-thread do:
synchronized(localBuffer)
{
  System.arraycopy(localBuffer, bufferedImageRasterBuffer, .....
}

with the local int[] and the BufferedImage's raster-buffer.
This way a currently painting BufferedImage can't block your CPU thread and the BufferedImage itself is never acced outside the AWT thread.
Only copying from the localBuffer to the bufferedImage's raster can block, but only short as this will be copied really fast.

2.) Really problematic parts are where two threads read&write, like the clipping stuff:
        private void setClippingRectangle(int aClipX, int aClipY, int aClipWidth, int aClipHeight)
is called by the CPU thread, but the results are later read on the event-thread.

Both reads and writes to such variables need to be synchronized on the same object. (public synchronized methodName() synchronized on "this").

3.) I don't know how you interact when AWT-Events modify stuff on the CPU-Thread, but here its the same.

PS: I hope I have not demotivated you, your project is great!

Good luck, lg Clemens
Offline broumbroum

Junior Devvie





« Reply #16 - Posted 2007-09-26 01:14:46 »

Just a few more suggestions:
(...)
Every time you access the local int[] synchronize it, but not in a loop, e.g.
synchronized(localBuffer)
{
  for(int i=0; i < localbuffer.lenght ....
}

In the event-thread do:
synchronized(localBuffer)
{
  System.arraycopy(localBuffer, bufferedImageRasterBuffer, .....
}

with the local int[] and the BufferedImage's raster-buffer.
This way a currently painting BufferedImage can't block your CPU thread and the BufferedImage itself is never acced outside the AWT thread.
Only copying from the localBuffer to the bufferedImage's raster can block, but only short as this will be copied really fast.
(...)
I might slightly correct the synchronization blocks, because there are some tricks not to fall in "dead-lock".
first, you may be blocked if both synchronized() {} blocks don't notify() each other, just as a Thread fails in sleep mode while the lock is active and waits forever.
and second, the localBuffer variable might refer to a new reference, which is likely the case when buffering pictures data (buffer is cleared or replaced), and therefore the thread won't lock to the correct reference if the variable changes the actual reference. Thereby a fixed monitor object reference must be used, that can be a single Monitor class you define with one member variable in it or even an usual Integer.


::::... :..... :::::: ;;;:::™ b23:production 2006 GNU/GPL @ http://b23prodtm.webhop.info
on sf.net: /projects/sf3jswing
Java (1.6u10 plz) Web Start pool
dev' VODcast[/ur
Offline Linuxhippy

Senior Devvie


Medals: 1


Java games rock!


« Reply #17 - Posted 2007-09-26 10:16:41 »

Well my comments were made with an eye to JStella's source, so should not be seen as universal and perfekt Wink

I might slightly correct the synchronization blocks, because there are some tricks not to fall in "dead-lock".
first, you may be blocked if both synchronized() {} blocks don't notify() each other, just as a Thread fails in sleep mode while the lock is active and waits forever.
Maybe I miss here something in my threading knowledge... If both threads never use wait and also don't use functions based on bait (like blocking IO, ...) is there really a possibility that a deadlock may occur?

Quote
and second, the localBuffer variable might refer to a new reference, which is likely the case when buffering pictures data (buffer is cleared or replaced), and therefore the thread won't lock to the correct reference if the variable changes the actual reference. Thereby a fixed monitor object reference must be used, that can be a single Monitor class you define with one member variable in it or even an usual Integer.
Well in the JStella case localBuffer was ment to be allocated in JSVideo and re-used all the time, to not stress the GC by allocating large arrays every frame.

lg Clemens
Offline broumbroum

Junior Devvie





« Reply #18 - Posted 2007-09-26 23:56:35 »

Well, it is much more complex to explain than to get it on one concrete example.
When I said "do not to miss notify() calls", it is such a rule that one programmer sticks to as he doesn't get it on the whole of its code... softly meant.
I put some on the shared code forum : http://www.java-gaming.org/forums/index.php?topic=17460.0  Cheesy

::::... :..... :::::: ;;;:::™ b23:production 2006 GNU/GPL @ http://b23prodtm.webhop.info
on sf.net: /projects/sf3jswing
Java (1.6u10 plz) Web Start pool
dev' VODcast[/ur
Offline Linuxhippy

Senior Devvie


Medals: 1


Java games rock!


« Reply #19 - Posted 2007-09-27 09:14:01 »

I read your post but I still don't see a need for notify calls, if synchronized is used on a monitor which is never used for wait() but only to disallow concurrent access and guarantee data-accasibility.
And if another monitor blocks and keeps the thread Waiting, a call to notify on the wrong monitor also does nothing...

I guess I did not understand your idea Wink

lg Clemens
Offline broumbroum

Junior Devvie





« Reply #20 - Posted 2007-09-27 17:52:38 »

I must apologize if my idea is unclear. But you're quite right, a notify() call is not needed everytime, but for security it is recommended to notify when exiting a synchronized block. As a matter of fact, though original Thread implementations do share on memory addresses references it is often the case that, when a data buffer or a variable is accessed by multiple Threads, the variable get on failure if it is read while another Thread is writing on it. That is such a mapping synch', which is impl. throughout synchronized Collections views, i.e. Collections.synchronizedMap/List/Set etc..
For example, say you want to animate your player with several images, if the animator don't synchronize on some monitor, the unsynchronized paint method will fail to find the correct index values to fetch the image. Thereafter if the animator keeps running for a period of time, not notifying release of the monitor would produce a dead-lock on painting, because the monitor is always re-acquired by the first animator Thread. Do you now better understand the idea of notifying? It's like sending messages to a mail-box and get notified when the mail-box receives new messages.

::::... :..... :::::: ;;;:::™ b23:production 2006 GNU/GPL @ http://b23prodtm.webhop.info
on sf.net: /projects/sf3jswing
Java (1.6u10 plz) Web Start pool
dev' VODcast[/ur
Offline Mauvila

Senior Newbie





« Reply #21 - Posted 2007-09-27 20:56:28 »

The finer points of threading are currently over my head.  Do either of you want WRITE access to the CVS? (I am also unsure about Applet threads, e.g. calling an Applet method via JavaScript...I've heard that start(), stop(), etc. are executed by a different thread, but I really don't know how this stuff all plays together.
Thanks,
JLA
Offline broumbroum

Junior Devvie





« Reply #22 - Posted 2007-09-28 00:43:35 »

Well this was about performances issue you're asking for. I don't want to get you bored with my "expert-explanations"... cu

::::... :..... :::::: ;;;:::™ b23:production 2006 GNU/GPL @ http://b23prodtm.webhop.info
on sf.net: /projects/sf3jswing
Java (1.6u10 plz) Web Start pool
dev' VODcast[/ur
Offline Mauvila

Senior Newbie





« Reply #23 - Posted 2007-09-29 21:39:28 »

I'm not totally sure threading is a big problem at this point...the graphic slow down issues have been resolved.  Occasionally when loading a game, there will be a thread incident, but I think overall everything works well.  I agree that the threading situation is not legit, but I don't see the negative consequences at this point. 
Thanks for your help,
JLA
Offline erikd

JGO Ninja


Medals: 16
Projects: 4
Exp: 14 years


Maximumisness


« Reply #24 - Posted 2007-10-01 10:04:18 »

As an aside, do you have any plans for supporting full-screen mode? I think JStella is great, but would be even better when being able to play full screen  Smiley

Offline broumbroum

Junior Devvie





« Reply #25 - Posted 2007-10-01 23:04:29 »

I'd also agree.... Smiley

::::... :..... :::::: ;;;:::™ b23:production 2006 GNU/GPL @ http://b23prodtm.webhop.info
on sf.net: /projects/sf3jswing
Java (1.6u10 plz) Web Start pool
dev' VODcast[/ur
Offline Mauvila

Senior Newbie





« Reply #26 - Posted 2007-10-02 04:26:24 »

I tried it once with disastrous results (on account of my experience with it).  If anyone wants to take a stab at it (or take over the project), let me know.  Medical school is devouring all my time.   
JLA
Pages: [1]
  ignore  |  Print  
 
 
You cannot reply to this message, because it is very, very old.

 

Add your game by posting it in the WIP section,
or publish it in Showcase.

The first screenshot will be displayed as a thumbnail.

trollwarrior1 (29 views)
2014-11-22 12:13:56

xFryIx (71 views)
2014-11-13 12:34:49

digdugdiggy (50 views)
2014-11-12 21:11:50

digdugdiggy (44 views)
2014-11-12 21:10:15

digdugdiggy (38 views)
2014-11-12 21:09:33

kovacsa (62 views)
2014-11-07 19:57:14

TehJavaDev (67 views)
2014-11-03 22:04:50

BurntPizza (65 views)
2014-11-03 18:54:52

moogie (80 views)
2014-11-03 06:22:04

CopyableCougar4 (80 views)
2014-11-01 23:36:41
Understanding relations between setOrigin, setScale and setPosition in libGdx
by mbabuskov
2014-10-09 22:35:00

Definite guide to supporting multiple device resolutions on Android (2014)
by mbabuskov
2014-10-02 22:36:02

List of Learning Resources
by Longor1996
2014-08-16 10:40:00

List of Learning Resources
by SilverTiger
2014-08-05 19:33:27

Resources for WIP games
by CogWheelz
2014-08-01 16:20:17

Resources for WIP games
by CogWheelz
2014-08-01 16:19:50

List of Learning Resources
by SilverTiger
2014-07-31 16:29:50

List of Learning Resources
by SilverTiger
2014-07-31 16:26:06
java-gaming.org is not responsible for the content posted by its members, including references to external websites, and other references that may or may not have a relation with our primarily gaming and game production oriented community. inquiries and complaints can be sent via email to the info‑account of the company managing the website of java‑gaming.org
Powered by MySQL Powered by PHP Powered by SMF 1.1.18 | SMF © 2013, Simple Machines | Managed by Enhanced Four Valid XHTML 1.0! Valid CSS!