Header Ads Widget

Ticker

6/recent/ticker-posts

Scrolling on the Amiga

Introduction

Depending on what your computer hardware can do, there are a number of different ways to approach displaying a scrolling game. We used to have to evaluate each machine that we were writing for, and then design our games around the advantages. That all comes unstuck, of course, when you're asked to convert a game from one platform to another.
 
Things to consider are: do we have user definable character modes?  Do we have bitmap modes? Do we have smooth scrolling in hardware? Do we have more than one playfield? Do we have hardware sprites? Do we have enough CPU time to rebuild everything on the screen every 50th of a second frame? Can we start the screen memory on any address in video RAM? Can we restart the screen display from another address during the display?
 

8-Bit Learning

Since we started on 8-bit machines, and only had the arcade games as an example of a better world, everyone was on the same learning processes. To get to the Amiga, we had to go through a number of other machines first.
 
To answer the above questions for each computer in turn:
 
Dragon 32
Do we have user definable character modes? No, character sets were fixed in ROM.
Do we have bitmap modes? Yes: 2 nasty 4-colour selections, and 2 2-colour selections. 
Do we have smooth scrolling in hardware?  No
Do we have more than one playfield? No
Do we have hardware sprites? No
Do we have enough CPU time to rebuild everything on the screen every frame?  No
Can we start the screen memory on any address in video RAM? No, only on 1K boundary
Can we restart the screen display from another address during the display? No
 
Chances of a smooth-scrolling game: sadly zero
 
48K ZX Spectrum
Do we have user definable character modes? No, only bitmaps.
Do we have bitmap modes? Yes: 320x200 (8K) with colour in 8x8 blocks. 
Do we have smooth scrolling in hardware?  No
Do we have more than one playfield? No
Do we have hardware sprites? No
Do we have enough CPU time to rebuild everything on the screen every frame?  No
Can we start the screen memory on any address in video RAM? No
Can we restart the screen display from another address during the display? No
 
Chances of a smooth-scrolling game: not at 50 frames per second. Using a lot of pre-rotated graphics in memory it's just about possible at 25 frames per second, if you're really clever - see Dominic Robinson.
 
Commodore 64
Do we have user definable character modes? Yes
Do we have bitmap modes? Yes: 320x200 2 colours or 160x200 4 colours (8K) with colour in 8x8 blocks. 
Do we have smooth scrolling in hardware?  Yes
Do we have more than one playfield? No
Do we have hardware sprites? Yes, 8.
Do we have enough CPU time to rebuild everything on the screen every frame? Yes
Can we start the screen memory on any address in video RAM? No, only  on 1K boundary.
Can we restart the screen display from another address during the display? No, but we can change the character set, for score panels and the like.

Having user-definable characters means we can hit 32 multi-colour pixels on screen in one go by changing one byte, which really saves a lot of time. By changing the actual graphic we can change all occurrences of the character on the screen at once - very powerful.
 
Chances of a smooth-scrolling game: excellent. Fast scrolling is possible because we have time to rebuild the entire background every frame, and control the sprites. We can also more economically slowly scroll and build a new buffer over, say 8 frames, and switch to a new 1K screen area as required.
 
Atari ST
Do we have user definable character modes? No, only bitmaps.
Do we have bitmap modes? Yes: 320x200 in 16 colours (32K).
Do we have smooth scrolling in hardware?  No, not until the STE.
Do we have more than one playfield? No
Do we have hardware sprites? No
Do we have enough CPU time to rebuild everything on the screen every frame?  Not really.
Can we start the screen memory on any address in video RAM? No, limited boundaries.
Can we restart the screen display from another address during the display? No
 
Chances of a smooth-scrolling game: Just about enough CPU time if you pre-rotate the graphics in all possible positions and possibly reduce the number of bit-planes/colours and reduce the play area by adding large control panel.
 
I did prototype Paradroid 90 with a 4-colour background scrolling in all directions, but we chose to go for a 16-colour version with only vertical scrolling because we felt that less colours would be unacceptable.
 
Commodore Amiga A500/A600/A1000
Do we have user definable character modes? No, only bitmaps.
Do we have bitmap modes? Yes: 320x200 or over-scanned in up to 64 colours (48K or more).
Do we have smooth scrolling in hardware?  Yes
Do we have more than one playfield? Yes, 2, if we limit each to up to 8 colours.
Do we have hardware sprites? Yes 4 16-colour 16-pixel wide sprites down the whole screen.
Do we have enough CPU time to rebuild everything on the screen every frame?  Just about.
Can we start the screen memory on any address in video RAM? Yes, any 2-byte boundary.
Can we restart the screen display from another address during the display? Yes

I did prototype Uridium 2 with a dual playfield mode. We had a 7-colour foreground and a 3 colour parallax background scrolling underneath. It was quite effective on its own, but with only 7 colours for the main background and any objects plotted onto it, distinguishing them was going to be really difficult. We could have used the hardware sprites for the Manta to make it stand out, but we needed to make the walls and the enemy ships and bullets really stand out too. We decided to use 32-colour graphics for the background instead.
 
The Amiga AGA chipset A1200 took the playfields further, allowing 2 playfields of 16 colours each, or 1 playfield with up to 256 colours from a palette of 16.8 million, and 2 MB of RAM. I'm told it had better/more hardware sprites, but I don't remember how that worked.
  

In an Ideal World

What we really wanted was what the arcade machines had. Before they got into sprite enlarging and rotation, and therefore into the 3D world, it looked like they had 2 or 3 playfields, 8x8 pixel characters that they could reflect individually in X or Y, many, many sprites that could go between or over the playfields, with reflection in X or Y, and in a number of different colour palettes. They clearly had smooth scrolling in X and Y too. That's my guess at what Taito had in 1987's Flying Shark, for example.
 

A Modern PC

I thought it would be interesting to compare and contrast a modern PC, which is not dissimilar in capabilities to a modern console in its approach.
 
Azumi , the PC
Do we have user definable character modes? No, only bitmaps.
Do we have bitmap modes? Yes: 800x600 up to 1920x1200 in 16 million colours (8MB).
Do we have smooth scrolling in hardware? No
Do we have more than one playfield? Can be done in software with layers.
Do we have hardware sprites? No
Do we have enough CPU time to rebuild everything on the screen every frame?  Yes
Can we start the screen memory on any address in video RAM? Controlled by OS, so no.
Can we restart the screen display from another address during the display? No

Whist above we started out with Nos and they became Yeses as the home computers improved, suddenly as the Amiga was swamped by consoles and PCs, the Yeses have become Nos again, with one exception: the Central Processor Unit (CPU) and the Graphics Processor Units (GPUs) now have enough time to rebuild the screen from scratch in a 60th of a second frame due to the multi-threading capabilities of each. You might find 2, 4 or even 6 CPU cores, and 50, 100 or even 1500 GPUs in your PC. Every pixel rendered can perform its own lighting calculation on one GPU. It's an impressive advancement, though it has not simplified the programmer's task, nor the graphics artists`. 
 

Some Background Information

In the old days our computer screens had borders. Nowadays the display is designed to exactly fit the screen, there is no border. You might think that's better, who needs a border? Well, we used to find it tremendously useful. We could assemble our programs in Debug mode, i.e. we could selectively switch in or remove lines of code, to set the border colour at the end of each major routine to a different colour.
We used interrupts synchronised to the picture display to control the game speed and give some stability to the border colours: they always started showing colours at the same stage on the screen, and you could make the scroll routine one colour, the screen restoration one colour, the object updates one colour and the plotters one colour. That way you would get a cascade of consistent colours down the sides and could see, for example, that updating all the objects might take a couple of character blocks down, the plotters might take 5, and so on. If a routine set half the screen border to one colour you knew it'd need some taming down. Plus, if it crashed, you'd be left with the border colour set to the last routine it didn`t finish.

We would have many compiler switches in a top-level header file to be able to control different configurations. One of them was:

No             equ 0
Yes            equ 1
MonitorDisplay equ No   ; Display CPU usage. Final build: No

We had an assembler macro called "Flash", which you could pass a colour value to, or in fact any valid assembler source such as a register, an immediate value or a variable:

Flash  Macro
    If MonitorDisplay=Yes
    move.w \1,COLOR00

    EndC
 EndM


By setting MonitorDisplay to Yes the macro will assemble the extra instruction to set the border colour. If set to No, it won`t assemble anything, so no time wasted. To use the macro you might have a piece of code like this:

  Flash  #$0500               ; Red
  bsr    WipeRadar

  Flash  #$0050               ; Green
  jsr    SpriteHandler

  Flash  #$0055               ; Cyan
  jsr    RestoreBack
  bsr    CharAnimateControl

  Flash  #$0005               ; Blue
  jsr    PlotSchedule
  bsr    BuildCopperList

  move.l ColourPalette\w,a0
  move.w (a0),d0
  Flash  d0


The final use of Flash sets the border to the colour it was supposed to be, usually black. You need to not make the colours too bright or the monitor starts to distort the line widths and it makes the screen difficult to read. Note that the Amiga colour zero is both the border and could be on the screen too.  
 
The graphics chips had to read the screen display memory, add the sprite data to the mix, and fire the resultant colours directly to the video output for the monitor to display all in exact timings to show a picture. At the bottom of the screen, the display is done, and has a rest as the raster crosses the lower border colour off the bottom before flying back to the top. It then displays some more border colour until the next display starts. The time when the graphics chip is not displaying data is called the "Vertical Blank" or VBlank interval. When the raster flips back to the top it`s called "Flyback". 

The Amiga was pretty flexible: we could set how many display lines we wanted to display on the screen, where they would start and end, and also we could set the left and right edge positions too. There were guidelines and standards so we stuck with those. Typically a PAL display had 312 possible display lines vertically, and we used 256 of them, at 50 frames per second. An NTSC display has 262 lines, so we had less vertical lines available, and it had to run at 60 frames per second. The display device was calling the shots. Nowadays the PC graphics card tends to tell the display what it`s going to get, and most PC displays run at 60 frames per second. There is a G-Sync system out there on some cards that allows variable frame timings whereby the screen waits until the display routine tells it there is a new screen available. Your frame rate become flexible, and because the display is not built by electrons hitting a phosphorescent screen, the LED or LCD screen just holds the last set of colour pixels it was sent until it gets new ones. Yes, you need a G-Sync monitor, graphics card and game software to do this. 

If you are plotting objects onto a bitmap screen rather than using hardware sprites, you really need to be using what we call double-buffering. Effectively we show one screen to the player and don't touch it, and then build the next screen in a second area of RAM, at our leisure, and when that's finished we can wait for the end of the display frame before swapping the buffers over, i.e. we present the new screen to the player and can start work on the old buffer.
 
Astonishingly it has taken PCs a long time to implement proper Vertical Syncing. How many times have you seen screen "tearing" where there is obviously some glitch going on across the screen; as the display is switched over to the back buffer while the screen is being displayed? The CPU had no way of detecting when the display of a whole frame was complete. On the Amiga we could set up interrupts to go off as soon as the vertical blank period started that would let us know it was safe to switch the screen buffers over.
 

To the Amiga Scroll Routine 

The Amiga display we used was made of 16x16 pixel tiles, for efficiency and flexibility. The CPU can write out twice that width but the tiles would be more difficult to work with, and collision detection would be grainier.
Since we don't really have enough CPU time to reconstruct a 40K screen from the 16x16 tiles from which the background is built, and since a fair amount of the screen doesn't change, we want to be able to use the data that is already there. This differs from the more modern PC and console games approach where there is usually a dynamic camera and we have to start every frame from scratch anyway. Most business software however only updates things on the screen that it knows have changed.

We need to be able to restore a screen back to its pristine self before we plot the new objects onto the background. otherwise things would leave trails. We could either mark the character blocks as dirty and restore the dirty ones as whole characters, or remember where we plotted the objects on the screen and copy just the area under the objects that has been disturbed. In fact, we sort of had to do a bit of both. We supported animated characters, such as the waterfalls in the jungle level 2 of Fire and Ice. In the event that we have updated the character animation then we need to update all the instances of those characters on the pristine screen buffer, and then in turn copy the data to the next 2 (or 3) back buffers to ensure that all of the screens show the same.
 
We chose not to mark characters as dirty because of the additional time to figure out which blocks to restore, and because a 4-pixel tiny object could dirty 4 entire characters if it was at the position where they meet. The plot routine already has to work out where it is going to plot on the screen, so we just store that information in a restoration list per screen.  
 
We also used hardware sprites to plot some objects. For those, we can ignore the masks and just copy the image to the sprite buffers. Additionally, we do not need to store any background restoration positions as the data is not plotted on the actual background.
 
The Amiga had a number of asynchronous operations going on. For example, there was a Copper chip. This could be given a series of instructions to  wait for specific raster lines on the screen and load hardware registers with values. This would be used to set the bitplane start addresses for the screen, since these change every frame because we are writing to 2 (or 3) screens. We also used it for colour effects, screen splits, and reflected images in the water. The Fire and Ice bottom panel at the waterline started reading the graphics data upside-down, altered the colours slightly and applied a sine-wave pattern to the X smooth-scroll position on each line.
 
We also have the Blitter chip, which can be used to copy data from A to B, or plot data onto the background (logically AND mask data at A to C, then logically OR data at B to C) . Since we interleaved our bit-planes, provided we interleaved 5 identical masks within our plot data, we could plot an object with one Blitter operation. This required more data, but means that you can let the Blitter get on with the whole plot while you work out the next object. We checked that the Blitter was ready for more action just before we have the data ready for the next Blit.  
 
Hardware sprites on the Amiga are not like the C64, more like the Atari 8-bit ones. You need to arrange all the objects you want to plot in sequence down the screen and then start putting them into the 4 lists, 1 for each 16-colour sprite. Each sprite list consists of an image, and then a wait instruction for the raster line when the next one starts. If we had too many hardware sprites on one raster line we would have to use the standard Blitter plot routine for the extra ones. In any case we can also use the Blitter to copy the image data to the sprite lists.
 

Putting Things in Order

It`s important to get things done in the right order. The right order depends on what sort of a game you are writing. There are games that scroll at a constant speed, such as Flying Shark, games that may scroll at a constant speed, or stop at times, such as Dragon Breed, or games where the player has control over where they want to go. I prefer the latter.
 
The player`s movement from the input device is used to adjust the player`s speed, and this decides the player`s new position. The player`s position on the map decides the player`s position on the screen, and depending on whether the player is locked in the middle or allowed to move around in a box in the middle of the screen, the player`s position on the screen decides the new required scroll position. In either case, the screen won`t scroll any faster than the player`s movement, which we have defined in the control mode.
 
We're always working in co-ordinates, the new scroll position is decided by simple tests that keep the player on the screen, and possibly in the middle. The actual scroll speed isn`t stored anywhere, we just have a new scroll position and a previous scroll position, and the routine has to do whatever it takes to align the new screen display for the player, regardless of where it was last time, which might be 2 moves hence in a double-buffered system, or 3 in a triple-buffered system.   
  

Vertical Scrolling First

Everything herein refers to Uridium 2, but most of it applies to Fire & Ice too. Fire and Ice used 16-colour graphics, whereas Uridium 2 used 32 colours on ECS chipset, and 64 colours on AGA chipset.
 
In order to preserve as much of the background as possible while scrolling, we need to use an Amiga trick of being able to restart the screen display at any point down the screen. The origin was always the top left, so let's start there.
 
Our background is made of 16x16 pixel characters. One could just as easily use 32x32 characters, or 8x8s. Our choice was based on efficiency against drawing flexibility. We did Rainbow Islands in 8x8s because that's what we thought Taito used, though they tended to arrange them in 2x2 blocks anyway.  
 
We had a map of the whole level laid out in RAM for quick access. This was just the character codes of the 16x16 graphics. Objects would need to know what characters they are flying over. All of the wall and high towers characters were at one end of the character set to give an easy cut-off point. Only the Mantas and their bullets actually collide with the background.
 
As we scroll the map downwards a pixel at a time, we start the screen bit-planes one raster line's worth of data later. The top character begins to recede under the top of the screen and a new character is revealed at the bottom. The screen buffer is one character taller (and wider) than the displayable screen. The Copper list has a static format and is updated during the vertical blank while it is not being used for display.
 
When we get to 16 pixels down, we have lost a whole character row at the top, it is no longer on display. What we now do is reuse the now-unseen top row of the screen with the graphics for the new row that's about to arrive at the bottom. Firstly we set up the graphics in the pristine restore buffer by copying the character graphics over, one by one. Then we can copy the entire rectangle one row deep by 20 characters wide from the pristine buffer to the back buffer area. We then have to deal with any animated characters, and also restore any areas that were covered by objects that will be moving on. The actual on-screen display will have moved twice (or three times) since its last display, so possibly a small area of restoration will be done that is unnecessary due to the new block being added, that's minimal and OK.
 
I've just remembered that some non-moving background features done with plotted objects could be drawn without transparent pixels and get plotted on the screen without saving the restore position because we know they're going to be re-plotted next frame. Hatchways and small gun turrets qualify. Every saving helps. These objects are also typically nicely aligned with the characters/bytes, so we can call the optimised plotter that knows it doesn't have to shift the graphic sideways into place. Rainbow Islands hidden fruit is an example. As long as the final frame of the animation matches what was on the background in the first place we can switch the animation off at the end and save plot time.
 
Now we have a background screen with a split near the top, which we need to aware of. As the screen scrolls downwards, the split moves upwards from the bottom at the same rate, and we have to tell the Copper chip to restart the bitplanes display at the top of the screen RAM. We also have to check for this split when we plot objects across the split, since we need to plot the top part of the object to one area of the screen buffer RAM, and the lower part to another. This results in 2 restore buffer entries also.
 
As the screen scrolls down further, the split moves up the screen until it hits the top, and then flicks back to the bottom again. You might think of the screen RAM buffer as a rolling barrel. We even referred to it as a barrel in the code.
 
If the screen scrolls upwards, the split moves down and we have to restore rectangles of characters at the top of the screen rather than the bottom. We had a routine for each, since it needs to be fast, though both do the same job, albeit based on different positions. It`s fastest if it doesn`t have to cater for 2 or more situations. Make the decision early when you can, and although it takes more space to code 2 near-identical routines, the saving of a few cycles gives you the more time for other things.
 

Horizontal Scrolling Second

This is the clever bit. We hadn't worked this out ourselves but we did have a chat on the phone with the Factor 5 team: Holger, Thomas and Julian. They had written Turrican II by this time and we had seen what they had achieved. They told us how to get the screen scrolling sideways using a similar technique to that used above, and all we had to do was allocate a few extra bytes at the end of our buffer, as many as we wanted to scroll one pixel row sideways from one side of the map to the other, in fact, say 20x2x10 bytes, i.e. 400 bytes per buffer, not much at all. 
 
So, starting from top left, we start smooth scrolling by 1 pixel to the right. This is done with the hardware scroll X register. What it's really doing is throwing away the number of pixels you tell it, from 0 to 15, before it starts displaying the screen picture. Our screen buffers always support one more character wide and high than the screen is; because in 15 out of 16 cases that extra character row or column is partially visible.
 
When you get to 16 pixels across you reset the Scroll X position to 0, and add one word (16 bits/pixels to the pointers to the start of the buffer display, and the split pointers, for each bitplane. You've now restarted your screen a whole character to the right. At this point we need to build the incoming column of characters to the right in our pristine buffer, and copy the rectangle of that buffer to our back buffer. All the data we need to display is untouched, and we have new data arriving on the right. You can keep going right for as much extra as the extra bytes we added. Going 10 screens worth to the right only costs 400 bytes. The whole screen is sliding sideways through the buffer.
 
To come back to the left, we just reduce the offset into the buffer by 2 bytes, or 16 pixels, at a time, keeping it synchronised with the smooth scroll X position, set up by the Copper list. We then add columns back on the left side and copy the rectangles from the pristine copy to the real screen buffers as they come round. 
 
Now all this does generate a lot of code, I won't deny it. The plot routines, the background refreshes, the character animations, the Copper list, everything has to work together. What we get out of it is an efficient system that doesn't cost us much time to just run the background, and each object only has to clean up the bytes that it alters. All of these components took quite a few weeks to code, and longer to get my head round it all.
 

Initialisation.

The easiest way to get the initial screens set up is to start the screen position 1 screen to the side of where we want. We then scroll the screen to the position we really want while it is blanked out, and since no game objects are active we can do this quickly, and we don`t have to wait for vertical blanks, so just the scroll routine will loop round 20 times if you do it horizontally, or only 12 if you go vertically. The same amount of work is done whichever you do because you're refreshing the whole screen either way.
 
Starting up any on-screen objects is usually also driven from the scroll movement, so by calling that as well, any on-screen objects are also started up by this initial scroll movement to set up the game graphics. All very convenient, it is.
 

Development

This scroll routine itself was coded at the beginning of Fire and Ice development. It took me a couple of weeks to get it done. You have to start with one character and build it up. I made up a set of basic blocks with numbers drawn on them so that I would be able to see that the correct characters were being displayed, and the animation could be seen. You can use the assembler to set up a simple map area so you don`t have to write a map editor and packer/unpacker yet.

Fire and Ice had an additional test mode since it supported slopes. Each character had a 16-bit word to define either the ground surface angle and height, or the side or ceiling blocking. On a key-press I could switch from showing the real characters to showing a wireframe diagram of the ground surface. This helped to see that the ground was continuous and the ice was smooth, and that there were no ways through that shouldn`t be there. I also had a ring of 2 blocked characters all the way round the map to make sure bullets, fish and birds stayed within the main play area. This eliminated the need for additional code to test for the map extremities as well as blocked characters.

As a point of note: due to the philosophy of not updating anything unnecessarily, the wire-frame map appears as you scroll along, at the leading edges or as objects move across the background, which also confirms how the restoration is working.

At first I hooked up the screen scroll position to the mouse input, so I could move the screen with the mouse. This input had less limitations, and of course I could add a multiplier to exaggerate or slow down the movement. This would prove the screen display could handle any movements. At this early stage of development there was no game, and no other objects to plot on the screen.

It is tough working on a screen display routine that is working completely on an unseen back buffer. There are lots of times when you don`t see anything happen on your display because you`ve used the wrong pointer, or got something else wrong. Usually there`s some evidence gets to the screen: a plotted object might splay out diagonally because you got the distance to the next row wrong, or it finishes early, or late. I don`t recall ever being able to set the screen to single-buffered mode so you could see stuff being written to screen.

Graphics are also removed from the screen by the restoration system so likely graphics would flicker and you'd be no better off in single-buffer mode. You have to write a lot of code and get it all working with will-power and staring at the code looking for mistakes. We started with a double-buffered system and then added the third buffer as an option later. Start as simply as you can. I'll do another page on the 16-bit plot routines for the Amiga soon. For example, we already had plot routines from the Atari ST, so we first adapted those for the Amiga bitmaps (easy few lines to change), and then altered them to generate Blitter commands later.
 

The Epilogue

When the game is at its busiest it is using all the available time and there will be little or no waiting time between frames. The slightest over-run into the next frame will cause the whole process to stall for a frame and the screen will not move smoothly for one frame. Naturally we design the game to run within the limits of the hardware whilst getting as much happening on screen as possible. Your game should not be generating meanies and bullets willy-nilly, you need to limit these elements to ensure over-runs don`t occur.
 
I have referred to a third background screen a few times. This is because we implemented triple buffering if there was extra RAM available. With a double-buffered system you have one screen on display, and another being worked on. Once the work is finished, you then have to wait for the raster to reach the end of the current display before you can swap the screens over. What if you could use the waiting time to start work on a third screen? You'd be getting ahead, so you could soak up any short over-runs if a few more bullets got fired.
 
When there is more RAM available on the ECS Amigas; it would be non-video RAM, called Fast RAM, because code executed there would run faster than in video RAM since the video chips might be using the video RAM, and will block the CPU. In those cases we would also load our game code into the Fast RAM where it would run faster as well. Double win! Bonus! On the AGA A1200s we get 2MB of video RAM and a faster CPU too. Party time!

Running a third buffer does allow us to get ahead, and allowed Mayhem Mode for the maniacs on the A1200 running the non-AGA version. The AGA version is permanently in near Mayhem mode. There is a slight downside in that the player may be looking at a frame that effectively happened 2 fiftieths of a second ago and what happened 1 fiftieth of a second ago is about to be displayed, and we're already building the next frame as well. No-one thinks that fast anyway, reaction time is about a tenth of a second so hopefully it doesn't make much difference. It can knock the sound effects ahead of the visuals by the same amount if you don't buffer the sound effects, which we didn't even think about! 
 
I've seen DirectX references to triple-buffering too, so I think it's a legit strategy if you're running on the ragged edge occasionally.
 
I'm considering in future projects doing a graphics performance test at the beginning or as the game progresses, to ensure that we don't over-run the magic sixtieth of a second. It`s the sort of thing to get done discretely during the titles sequence. There should be plenty of time for effects, and they can be tamed, and of course the user expects to be given overall control of the screen size/resolution and some of the more advanced rendering features.
 

 
 
 
 
 

Yorum Gönder

0 Yorumlar