Forums

Sega Master System / Mark III / Game Gear
SG-1000 / SC-3000 / SF-7000 / OMV
Home - Forums - Games - Scans - Maps - Cheats - Credits
Music - Videos - Development - Hacks - Translations - Homebrew

View topic - How many bytes can I write to VRAM per frame?

Reply to topic
Author Message
  • Joined: 05 Sep 2013
  • Posts: 2915
Reply with quote
How many bytes can I write to VRAM per frame?
Post Posted: Tue Oct 15, 2013 3:39 pm
[Admin: split from http://www.smspower.org/forums/viewtopic.php?t=14373 ]

How many bytes can I expect to be able to write to VRAM during a VBlank? Is reading from ROM slower than reading from RAM?
(edit: I mean on a SMS)
  View user's profile Send private message Visit poster's website
  • Joined: 31 Oct 2007
  • Posts: 852
  • Location: Estonia, Rapla city
Reply with quote
Post Posted: Tue Oct 15, 2013 7:39 pm
Assuming 192 line tall active display :

* 342 pixels per line
* 313 lines in PAL
* 262 lines in NTSC

* 1 slot every 4 pixels
* 1 slot every 2 on MD, with refresh cycles every 64 pixels
* 85 slots per passive line
* 10 slots per active line

* 228 CPU cycles per line

* 53693175 / 5 / 262 / 342 / 2 = 59.92Hz
* 53203424 / 5 / 313 / 342 / 2 = 49.70Hz

* Tile is 32 bytes


Number of bytes that can be transferred using Z80 (OUTI):
+------+--------+---------+-------+
| Mode | Active | Passive | Total |
+------+--------+---------+-------+
| PAL  |  1920  |   1724  |  3644 |
| NTSC |  1920  |    997  |  2917 |
+------+--------+---------+-------+
Number of tiles that can be transferred using Z80 (OUTI):
+------+--------+---------+-------+
| Mode | Active | Passive | Total |
+------+--------+---------+-------+
| PAL  |   60   |    53   |  113  |
| NTSC |   60   |    31   |   91  |
+------+--------+---------+-------+
  View user's profile Send private message Visit poster's website
  • Joined: 05 Sep 2013
  • Posts: 2915
Reply with quote
Post Posted: Wed Oct 16, 2013 12:46 pm
Interesting doc!
If I get it correctly, on a NTSC machine at 192 lines mode I should be able to transfer up to 997 bytes during the VBlank, right?
Unfortunately though I can't understand how to calculate how many bytes I should be able to transfer when in 224 lines mode :|
Any help?
  View user's profile Send private message Visit poster's website
  • Site Admin
  • Joined: 19 Oct 1999
  • Posts: 13663
  • Location: London
Reply with quote
Post Posted: Wed Oct 16, 2013 3:10 pm
Last edited by Maxim on Wed Oct 16, 2013 3:24 pm; edited 1 time in total
There's a bunch of working left as an exercise for the reader above, but in NTSC 224 line mode you get 2240 + 541 = 2781 bytes.
  View user's profile Send private message Visit poster's website
  • Joined: 14 Apr 2013
  • Posts: 532
Reply with quote
Post Posted: Wed Oct 16, 2013 3:12 pm
sverx wrote
Interesting doc!
If I get it correctly, on a NTSC machine at 192 lines mode I should be able to transfer up to 997 bytes during the VBlank, right?
Unfortunately though I can't understand how to calculate how many bytes I should be able to transfer when in 224 lines mode :|
Any help?


There are 262 lines in NTSC. If 192 of these lines are active there are 70 (262 - 192) passive lines. If 224 lines are active there are 38 (262 - 224) passive lines. So if you can transfer 997 bytes while there are 70 lines you can transfer ~541 (997 * 38/70) bytes.
  View user's profile Send private message Visit poster's website
  • Joined: 05 Sep 2013
  • Posts: 2915
Reply with quote
Post Posted: Wed Oct 16, 2013 3:39 pm
Thanks! Very few, indeed... SAT is 64+128 bytes...

BTW I'm already experiencing problems, unfortunately. Can't make any sprite appear. The sprite palette (palette 1) is loaded, patterns (tiles) are loaded at $2000 and SPG is $2000 according to this, SAT is at $3F00 and it's loaded... I can see everything correctly on MEKA's tile viewer / palette viewer and in Memory viewer, there are no tiles on BG having priority (and the BG tiles are all zeroed out BTW...) but I see nothing on the screen :|

What's wrong? I've been searching for a 'master sprite enable' bit or something like that, but I see there shouldn't be any, so I really don't get what I still need to do to see that sprite where it's supposed to be...

Thanks in advance!

edit: oh, nevermind... it was the enable display bit in register $01, sorry for that, I thought it was on :|
  View user's profile Send private message Visit poster's website
  • Joined: 05 Sep 2013
  • Posts: 2915
Reply with quote
Post Posted: Thu Oct 17, 2013 3:18 pm
Wait a second...

342 pixels per line, 1 slot every 4 pixels. Since there are 256 pixel on an active line, there should be 86 pixel remaining, which should turn into 86/4=21,5 slots per active line, not 10 ... or am I missing something again???
  View user's profile Send private message Visit poster's website
  • Joined: 31 Oct 2007
  • Posts: 852
  • Location: Estonia, Rapla city
Reply with quote
Post Posted: Thu Oct 17, 2013 11:33 pm
There's more on a line than just the visible 256 pixels. Total line length is 342 pixels, all the rest is blanking, sync and porch areas.
On active line there is one access slot every 32 pixels, on passive there's one every 4 pixels.
  View user's profile Send private message Visit poster's website
  • Site Admin
  • Joined: 19 Oct 1999
  • Posts: 13663
  • Location: London
Reply with quote
Post Posted: Fri Oct 18, 2013 8:26 am
The H blank time is used to load the sprites for the next line. I don't know if you get extra access time if the sprite table is terminated early. Getting the timing right to write faster during H blank would be tricky too.
  View user's profile Send private message Visit poster's website
  • Joined: 05 Sep 2013
  • Posts: 2915
Reply with quote
Post Posted: Fri Oct 18, 2013 2:31 pm
I'm evaluating if it's worth and if it could work... the idea is that I need to load from ROM up to 40 tiles each frame, and since it seems to me I really can't do all that while in VBlank, I have to do that using the HBlank intervals... if I can copy 8 bytes each line I would need 160 lines, which makes that theoretically possible...
Any suggestions from the experts here? :)
  View user's profile Send private message Visit poster's website
  • Site Admin
  • Joined: 19 Oct 1999
  • Posts: 13663
  • Location: London
Reply with quote
Post Posted: Fri Oct 18, 2013 3:01 pm
Just load them slowly during the active display, if you can afford to double-buffer between the displayed and updating tiles.
  View user's profile Send private message Visit poster's website
  • Joined: 05 Sep 2013
  • Posts: 2915
Reply with quote
Post Posted: Fri Oct 18, 2013 3:22 pm
You mean doing that during the active phase on each line? I understood it was impossible to write to VRAM when not in H/V blanking... :|
  View user's profile Send private message Visit poster's website
  • Joined: 31 Oct 2007
  • Posts: 852
  • Location: Estonia, Rapla city
Reply with quote
Post Posted: Fri Oct 18, 2013 5:50 pm
Hblank is not free time, its all used up by sprite processing. Line is entirely active or entirely passive. Passive ones are all in VBL area, all else is active and limited to few slots.
  View user's profile Send private message Visit poster's website
  • Site Admin
  • Joined: 19 Oct 1999
  • Posts: 13663
  • Location: London
Reply with quote
Post Posted: Fri Oct 18, 2013 8:58 pm
The "active" figures in TmEE's posts above refer to the active display period. You can write every 21 cycles which turns out to be quite a lot of bytes.
  View user's profile Send private message Visit poster's website
  • Joined: 05 Sep 2013
  • Posts: 2915
Reply with quote
Post Posted: Sun Oct 20, 2013 10:56 am
Wow! ... it turns out I actually misunderstood almost everything! :|
So, let's see if I get it correctly. If I want to write to VRAM during an active line I can do that with no special care needed but I'll be able to write just very few bytes (up to 10) and no more. On passive lines instead I can write up to 85 bytes each line. Number of available active/passive lines depends not only on actual vertical resolution (192/224/240) but also on SMS 'version'... if it's PAL you got more total lines than NTSC.

If all that is correct, does that mean also that if I run, let's say, 30 OUT opcodes when the screen refresh is starting the last one of them completes at the end of the 3rd line? (I'm actually asking if the OUT operation gets 'sync'ed' with the VRAM access slot or not...)
  View user's profile Send private message Visit poster's website
  • Joined: 31 Oct 2007
  • Posts: 852
  • Location: Estonia, Rapla city
Reply with quote
Post Posted: Sun Oct 20, 2013 6:30 pm
That is correct except Z80 can never saturate VRAM bandwidth in blanking. You can push data as fast as you can and never hit the max. Z80 can only use up about 20% of the VRAM bandwidth.
On active lines you only have to make sure you don't go beyond 10 bytes per line. Any writes between access slots will be discarded.
Emukon attempts to emulate it, most emulators not.
  View user's profile Send private message Visit poster's website
  • Joined: 05 Sep 2013
  • Posts: 2915
Reply with quote
Post Posted: Mon Oct 21, 2013 9:58 am
So if I start pushing bytes at the beginning of the first line I'll probably just get the first 10 bytes written and the other bytes discarded? That's not what I was planning :|

There's no such problem during VBlank, right? I mean, I can push bytes with no problems as long as VBlanking lasts, or is there again some special care I should put?

edit: there is no DMA or similar stuff in the SMS/GG or am I wrong?
  View user's profile Send private message Visit poster's website
  • Joined: 31 Oct 2007
  • Posts: 852
  • Location: Estonia, Rapla city
Reply with quote
Post Posted: Mon Oct 21, 2013 10:39 am
Yes, you cannot just push in 10 bytes all at once, you have keep the spacing in active lines.

In VBL area you can have most mad code you can create to get things happen, VDP will be able to eat it up all the way.

General idea is that you prepare your game stuff during active lines and once done you'll look at transferring all the GFX into VRAM. I created a VRAM transfer queue that game code fills up and whole VBL is spent tranferring all the stuff. I create forced blank condition during that time and won't enable display until all is tranferred, this ensures I won't hit active lines, but at cost of posible flickering in top of the frame, but whole lot better than GFX garbage.

There is no DMA in SMS/GG, if there were NES would by crying in shame...
  View user's profile Send private message Visit poster's website
  • Joined: 05 Sep 2013
  • Posts: 2915
Reply with quote
Post Posted: Mon Oct 21, 2013 11:15 am
I see, and it's very interesting your approach of not re-enabling the display until everything has been transferred, so that you get more passive scanlines, since my problem actually is that I calculated I really can't transfer all the bytes I need during VBlank...

Thanks!
  View user's profile Send private message Visit poster's website
  • Joined: 05 Sep 2013
  • Posts: 2915
Reply with quote
Post Posted: Mon Oct 21, 2013 8:23 pm
TmEE wrote
[...] I created a VRAM transfer queue that game code fills up and whole VBL is spent tranferring all the stuff. I create forced blank condition during that time and won't enable display until all is tranferred, this ensures I won't hit active lines, but at cost of posible flickering in top of the frame, but whole lot better than GFX garbage. [...]


How many bytes can you trasfer this way? I'd need to trasfer up to almost 3KB... is it too much dreaming?

edit: BTW I don't get that table figure that say that you can transfer up to 997 bytes when in VBlank (when in NTSC 192 scanlines mode) where it comes from. :|
If NTSC is 262 lines and there are 192 active lines, aren't there 70 passive lines? And isn't it possible to transfer up to 85 bytes for each one of those lines? This would be almost 6KB!
  View user's profile Send private message Visit poster's website
  • Joined: 31 Oct 2007
  • Posts: 852
  • Location: Estonia, Rapla city
Reply with quote
Post Posted: Tue Oct 22, 2013 12:18 pm
85 bytes is indeed possible, but the poor Z80 only manages 20% of the total, you will not reach that speed without DMA or some other means.
OUTI instruction takes 16 cycles IIRC, and you only got 228 cycles per line.
  View user's profile Send private message Visit poster's website
  • Joined: 05 Sep 2013
  • Posts: 2915
Reply with quote
Post Posted: Tue Oct 22, 2013 2:02 pm
I'm a bit puzzled now :|
How can 85 bytes be pushed each line if it's possible to issue only some 14 OUTI?
[Even if in effect 228/16*70 (the passive lines) gives that expected 997 figure mentioned before...]

I guess I'm just trying to achieve too much from that humble machine :\
  View user's profile Send private message Visit poster's website
  • Joined: 31 Oct 2007
  • Posts: 852
  • Location: Estonia, Rapla city
Reply with quote
Post Posted: Tue Oct 22, 2013 2:07 pm
VDP has one access slot every 4 pixels, Z80 cannot hit all of them, only some. Z80 can hit about every 4th slot, rest are missed.

When you want to tranfer too much you'll have to do reduced vertical res stuff, or redesign your concept to use less data.
I design around the limits but allow handling of beyond limit without severe consequences.
  View user's profile Send private message Visit poster's website
  • Joined: 05 Sep 2013
  • Posts: 2915
Reply with quote
Post Posted: Tue Oct 22, 2013 3:15 pm
Yeah, I think I'll have to resize my plan to a lower scale. The idea was to push in 36 tiles from ROM each frame, update SAT and PNT and I see it's really too much, especially when in NTSC 224 lines mode.

I might consider PAL/192 lines, though... well, let's see.
  View user's profile Send private message Visit poster's website
  • Joined: 05 Sep 2013
  • Posts: 2915
Reply with quote
Post Posted: Wed Oct 23, 2013 3:29 pm
Another problem is that the PNT is quite big... 32x28 and each entry is 2 bytes, so updating it completely each frame it's almost out of discussion, if I want to be able to do something else (for instance update SAT, or eventually loading in some -few!- tiles from ROM).

Any suggestions? What's most/your games approach? (Double buffering won't help that much, I believe, since at the end a whole copy is needed again... maybe a 'dirty' flags map?)
  View user's profile Send private message Visit poster's website
  • Joined: 31 Oct 2007
  • Posts: 852
  • Location: Estonia, Rapla city
Reply with quote
Post Posted: Wed Oct 23, 2013 3:32 pm
What exactly do you want to do that you have to update whole sprite table and tilemap ?

Scrolling game would only have to update edges of the tilemap as you scroll through it.
Most games are not even using half the sprites available, you only have to update as much as is actually used...
  View user's profile Send private message Visit poster's website
  • Joined: 05 Sep 2013
  • Posts: 2915
Reply with quote
Post Posted: Wed Oct 23, 2013 3:41 pm
I didn't mean to say I have to update the whole PNT, and I am also planning of using half of the available sprites (thus updating only half of the SAT too)... but in my plans I have some background animations going on, but no scrolling.

One idea I'm evaluating is to build up a list of the pattern I have to change in the PNT (something simple, say an address and a value pair for each one until it's done, using some value -zero?- to mark that the list is over...) and go thru the list during VBlank, but I don't know how many updates I can expect to do this way.

Should I code it and count cycles? I guess it's the only unequivocal answer, right? :|
  View user's profile Send private message Visit poster's website
  • Joined: 31 Oct 2007
  • Posts: 852
  • Location: Estonia, Rapla city
Reply with quote
Post Posted: Wed Oct 23, 2013 3:53 pm
You don't animate single units every single frame, so you can spread out your animations over several frames, i.e update some stuff on one frame and others on next and so on until you are back to original stuff that needs animating.
  View user's profile Send private message Visit poster's website
  • Joined: 05 Sep 2013
  • Posts: 2915
Reply with quote
Post Posted: Thu Oct 24, 2013 9:34 am
True, indeed.
But for sure sometimes I'll have to update a whole row and a column (and maybe even few more here and there) in the same frame, I hope this is feasible... let's say update 60 entries (thus 120 bytes)... this should be quite feasible right?
  View user's profile Send private message Visit poster's website
  • Joined: 31 Oct 2007
  • Posts: 852
  • Location: Estonia, Rapla city
Reply with quote
Post Posted: Thu Oct 24, 2013 9:41 am
That would work out.
  View user's profile Send private message Visit poster's website
  • Joined: 05 Sep 2013
  • Posts: 2915
Reply with quote
Post Posted: Thu Oct 24, 2013 12:39 pm
I think so, even if I just realized I also have to write to VDP Control Port two bytes to select the VRAM address I want to write to, before pushing the two bytes I need. That would make 240 OUTI for 60 updates... great :|
  View user's profile Send private message Visit poster's website
  • Site Admin
  • Joined: 19 Oct 1999
  • Posts: 13663
  • Location: London
Reply with quote
Post Posted: Thu Oct 24, 2013 1:05 pm
Well, if you're doing 4-way scrolling then you will need (at most, assuming motion is no more than 8px per frame in either direction) to seek and write 64 bytes for the vertical scrolling (one row) and to seek and write 2 bytes, 24 times for the horizontal scrolling (ignoring the overlap). That's 114 OUTIs plus possibly some glue logic if you haven't totally pre-prepared the write buffer. There are some tricks you can do regarding updating the LSB of the VRAM address pointer, but I'm not sure it's very compatible.
  View user's profile Send private message Visit poster's website
  • Joined: 05 Sep 2013
  • Posts: 2915
Reply with quote
Post Posted: Thu Oct 24, 2013 3:35 pm
I've read about that LSB thing of the address pointer, but I won't very often update rows (or big fractions of it) even if I will surely have to update 2 (or even more) patterns in a row quite often.
So my first try is a very simple approach:

; this updates the PNT during VBlank using the info from
; PNTUpdatesTable and PNTUpdatesCount
; each entry in the table is as follows:
; VRAM address (two bytes - includes 'mask' bits), PatternName (two bytes)
updatePatternNamesTable:
  ld hl,PNTUpdatesCount
  ld b,(hl)
  xor a                   ; a = 0
  cp b                    ; b = a ?
  ret z                   ; if so, nothing to update, leave
  ld hl,PNTUpdatesTable
-:ld c,VDPControlPort
  outi
  outi
  ld c,VDPDataPort
  outi
  outi
  jp nz,-                 ; b != 0 ?
  ret


Suggestions? Please be kind, it's almost the first block of Z80 asm code I write in my life :D
  View user's profile Send private message Visit poster's website
  • Site Admin
  • Joined: 19 Oct 1999
  • Posts: 13663
  • Location: London
Reply with quote
Post Posted: Thu Oct 24, 2013 3:57 pm
For row updates, and also any other VRAM writes (e.g. tile animation) it'd make more sense to support more than 2 bytes at a time, for speed. Either go with a more general-purpose routine with specified byte counts, or the opposite: make this for updating 24 elements vertically in the name table, making it always loop 24 times, and write a similar one for the 64 bytes of a row update, and another for 32-byte tile writes, each with their own control structures.
  View user's profile Send private message Visit poster's website
  • Joined: 05 Sep 2013
  • Posts: 2915
Reply with quote
Post Posted: Thu Oct 24, 2013 6:38 pm
True... when I was commuting home this afternoon I was thinking that since the most common transfer will be 2 subsequent entries (thus 4 bytes) I'd better make something different, so I guess the first -basic- approach will be something like having a counter for the bytes to transfer followed by the target address and the actual data for each transfer block, stopping when the first byte (the counter) says zero, that would mean the blocks are over... this doesn't sound too much complicated :)
  View user's profile Send private message Visit poster's website
  • Joined: 05 Sep 2013
  • Posts: 2915
Reply with quote
Post Posted: Fri Oct 25, 2013 1:22 pm
I ended up doing this... but I'll prepare a different function for pushing patterns (tiles) into VRAM, since that needs to copy exactly 32 bytes from a given location in ROM (or eventually from RAM)

; this updates the PNT using the info from PNTUpdatesTable
; each entry in the table is as follows:
; - byte count (one byte: zero means the whole table is over)
; - VRAM address (two bytes, the address includes 'mask' bits)
; - PatternName data ('count' bytes)
updatePatternNamesTable:
  ld hl,PNTUpdatesTable
  xor a                   ; a = 0
-:ld e,(hl)
  cp e                    ; e = 0 ?
  ret z                   ; if so, we've done : leave
  inc hl

  ld c,VDPControlPort     ; write the address
  outi                    ; to the VDP
  outi                    ; control port

  ld b,e                  ; b = count
  ld c,VDPDataPort        ; write to the VDP data port
  otir                    ; 'count' bytes
  jp -


Any suggestion is welcome :)
  View user's profile Send private message Visit poster's website
  • Site Admin
  • Joined: 19 Oct 1999
  • Posts: 13663
  • Location: London
Reply with quote
Post Posted: Fri Oct 25, 2013 4:25 pm
For larger blocks of data, it is faster to jump into a block of outi opcodes than to use otir; the tradeoff is in the time it takes to calculate where to jump. As an alternative, instead of storing the byte count in the data, store the jump destination (calculated outside of VBlank when CPU time is cheap).
  View user's profile Send private message Visit poster's website
  • Joined: 05 Sep 2013
  • Posts: 2915
Reply with quote
Post Posted: Mon Oct 28, 2013 9:18 am
That's a good suggestion. I also thought that most of times I already know the bytes I have to push into VRAM so instead of having the data in the PNTUpdatesTable I could simple store there a pointer to the place in ROM (or eventually in RAM) where the data is.
Of course this would make things slower for very short data transfers, so I might handle them with a separate list, if I start struggling with performance (which I believe would probably happen...)

[slightly off-topic] Are there SMS games that have their game logic running at a fraction of the screen refresh? (I mean such as 30 fps, 25 fps, 20 fps and so on...). Is that quite common or absolutely not?
  View user's profile Send private message Visit poster's website
  • Site Admin
  • Joined: 19 Oct 1999
  • Posts: 13663
  • Location: London
Reply with quote
Post Posted: Mon Oct 28, 2013 10:23 am
If you're going for maximum VRAM writes per VBlank then you will need to avoid indirection and do as much work as possible before the VBlank. Dedicated name table H and V scroll updating routines, with precached data, will be fastest, likewise for sprites, palette, etc.

Plenty of games run at less than 60fps, some later games even do 15fps or less. In Meka, hit Alt+F12 to frame advance and see how many times you press it between actual updates... some games even update the background and sprites in separate frames, although that looks pretty bad.
  View user's profile Send private message Visit poster's website
  • Joined: 05 Sep 2013
  • Posts: 2915
Reply with quote
Post Posted: Mon Oct 28, 2013 10:55 am
Maxim wrote
If you're going for maximum VRAM writes per VBlank then you will need to avoid indirection and do as much work as possible before the VBlank.


I understand. Anyway I still have to check if I can manage to afford that indirection during VBlank, because otherwise it would be quite a waste to simply copy from ROM to RAM some let's say 60 successive bytes just to avoid that indirection...
  View user's profile Send private message Visit poster's website
  • Joined: 05 Sep 2013
  • Posts: 2915
Reply with quote
Post Posted: Mon Oct 28, 2013 3:41 pm
Speaking about having an OUTI block instead of an OTIR instruction, I don't get how to CALL some specific address (such as the 10th OUTI in the block) when I don't know (until runtime) how many OUTI I should issue... CALLs instructions can get only immediate addresses, isn't it so? :|
  View user's profile Send private message Visit poster's website
  • Site Admin
  • Joined: 19 Oct 1999
  • Posts: 13663
  • Location: London
Reply with quote
Post Posted: Mon Oct 28, 2013 4:24 pm
There's a trick:

  ld hl,<some number you only know at runtime>
  call callHL
  <do more work>
  ...


callHL:
  jp (hl)

Credit to Heliophobe for bringing this to my attention. If you have nothing to do after the call callHL then you should do the jump directly (i.e. tail call optimisation). Alternatively, you can push a return address of your choice on the stack and then jump.

The hard part about using this with the outi hack is that you need the value in hl, so you may have to use an ix/iy variant at the cost of a few cycles.

Another way is to modify some code in RAM so you overwrite the immediate address on a call opcode.
  View user's profile Send private message Visit poster's website
  • Joined: 05 Sep 2013
  • Posts: 2915
Reply with quote
Post Posted: Tue Oct 29, 2013 11:07 am
Nice trick indeed! :) I also was thinking about having some opcodes modified in RAM, but I also thought that maybe it wasn't worth.

BTW I also verified that making things some clever way [*], I can update everything I need using less than 450 OUTI instructions each frame, which should be feasible in VBlank even in NTSC/224 lines mode :)

[*] a mix of everything discussed here. So, a specific function to push 4 successive sprite tiles to VRAM, a specific function to change 30 adjacent background tiles in PNT, a specific function to change 2 adjacent tiles in PNT plus 2 adjacent tiles just under the previous 2... then another specific function to change a single tile... all those using the OUTI block for cycle saving... and finally a generic function to handle what isn't feasible to do with the previous, which should happen almost never ;)
  View user's profile Send private message Visit poster's website
Reply to topic



Back to the top of this page

Back to SMS Power!