Jump to content
IGNORED

PM multiplexor


Heaven/TQA

Recommended Posts

another version... now the 5th... with less gfx glitches... still do not know what causes the dli2 to jump sometimes...

 

when booted in atari800win press f5 for quick reboot, the default move table is altered randomly each boot...so you'll see different move patterns...

 

8 sprites handled at the moment... in this version i have switched back to version 3 in terms of DLI amount and mapping logic...so now i am back to "DLI regions" and not 1 DLI per sprite...do not know if this is good or not...

plex5.zip

Link to comment
Share on other sites

another disadvantage for the player/missle concept...you can not easily remove sprites by setting them to "non visible" areas or to skipp...

 

think of the following...

 

frame 1 sprite sits at 100,100---> mapper copies the sprite into the desired player/missle area... then frame 2 sprite is marked as to be rejected which means invisible... the engine needs to delete now the sprite data and this takes cpu time... which f.e. on c64 you don't need...

as sprite data is handled like on GBA ("tiles").

Link to comment
Share on other sites

another disadvantage for the player/missle concept...you can not easily remove sprites by setting them to "non visible" areas or to skipp...

 

I never said that the PMg is the same like C64s Sprites ;)

 

 

think of the following...

frame 1 sprite sits at 100,100---> mapper copies the sprite into the desired player/missle area... then frame 2 sprite is marked as to be rejected which means invisible... the engine needs to delete now the sprite data and this takes cpu time... which f.e. on c64 you don't need...

as sprite data is handled like on GBA ("tiles").

 

Why not "simply" using another DLI to put the Player off the screen, when it is not needed?

Link to comment
Share on other sites

Heaven

 

For a better understanding

How are you moving the Sprites?

 

copy...clear....move....copy.... clear...move

 

or moving the sprite data in the PM memory(0 space 1 PM DATA) by simply copying always PM data including spaces?

 

"0111111110"

abcdefghijklmnop

01111111100000

00111111110000

00011111111000

 

The last method will need always two bytes more to handle, but you don't have to clear anything?!?!

Link to comment
Share on other sites

Heaven

 

For a better understanding

How are you moving the Sprites?

 

copy...clear....move....copy.... clear...move

 

or moving the sprite data in the PM memory(0 space 1 PM DATA) by simply copying always PM data including spaces?

 

"0111111110"

abcdefghijklmnop

01111111100000

00111111110000

00011111111000

 

The last method will need always two bytes more to handle, but you don't have to clear anything?!?!

 

That's useless when the stepsize of the movement is 2 or more.

F.e. stepsize 2 gives:

 

011111110000000

0101111111100000

0101011111111000

..etc.

 

In most cases we're not restricted to just stepsize 1.

 

A standard method (I'm using) is:

-get old y-value

-clear old sprite

-get new y-value

-move new sprite

Never copy from PMBASE to PMBASE

 

-----

mux

Link to comment
Share on other sites

@ emkay

 

at the moment it works like this:

 


vbl                  jsr remapper    ;clears old DLI bits

 jsr sprite_sort

 jsr move

 jsr mapper

 

and the sprite looks like

 


sprite	dta %00000000

 dta %00000000

 dta %00000000

 dta %00000000

 dta %00000000

 dta %00011000

 dta %01111100

 dta %11111110

 dta %11111111

 dta %11110011

 dta %01110011

 dta %00111100

 dta %00011000

 dta %00000000

 dta %00000000

 dta %00000000

 dta %00000000

 dta %00000000

 

as you can see i have "dummy bytes" included to avoid the clearance of the sprites when moving vertical...

 

another dli to remove sprites will probably not work. why?

 

look:

 


ascii art:



assume this sprite position on screen



12345

....6

....7

 

positions would be

 

sprite_nr(x,y)

 

sprite_1(10,10)

sprite_2(18,10)

sprite_3(26,10)

sprite_4(34,10)

sprite_5(42,14)

sprite_6(42,22)

sprite_7(42,30)

 

spritesize is 8x8

 

the sprite sorter would organise the sprites in this order:

 

1,2,3,4,5,6,7 (y-direction)

 

the sorter would test next sprites5,6,7 for overlapping sprite 1,2,3

 

for nr=4 to 7

if (sprite_y[nr]-sprite_y[nr-4])<8 then mark them as "to be rejected = sprite_y(nr-4)=0.

next nr

 

for sprite5 the result would be 14-10=4 --> overlapping

for sprite6 the result would be 22-10=12 --> not overlapping = secure

 

sprite_y-coordinates=0 is the rejection marker for all other routines because it can easily tested with "BEQ" or "BNE".

 

after the sorting & testing the DLI bits are set depending on the y-positions. as 4 sprites can always be set in 1 DLI the mapper is now setting the DLI bits in the displaylist (antic e for simplicity)

 


mapper	ldx sortspry	;set 1st DLI for the sprites 0-3 this DLI is always possible

 ldy dl_lookup,x

 lda dlisti,y	;get antic command

 sta dlist,y  ;write into diisplay list

 

 lda sortspry+4	;set DLI for sprites 4-7

 bne	mapper_aa	;0=rejected sprite and will be skipped -> next sprite

 lda sortspry+5

 bne mapper_aa

 lda sortspry+6

 bne mapper_aa

 lda sortspry+7

 bne mapper_aa

 jmp copy
;  sub	sortspry

mapper_aa sub #dli_secure

 tax

 ldy dl_lookup,x

 lda dlisti,y

 sta dlist,y



mapper_a lda sortspry+8	;set DLI for sprites 8-12

 bne mapper_bb

 lda sortspry+9

 bne mapper_bb

 lda sortspry+10

 bne mapper_bb

 lda sortspry+11

 bne mapper_bb

 jmp copy
;  sub	sortspry

mapper_bb sub #dli_secure

 tax

 ldy dl_lookup,x	;display list lookup table

 lda dlisti,y

 sta dlist,y

 jmp copy

 

mapper_b lda sortspry+12	;set DLI for sprites 12-16

 beq copy
;  sub	sortspry

 sub	#dli_secure

 tax

 ldy dl_lookup,x

 lda dlisti,y

 sta dlist,y



 

i have 3 displaylists in memory

 

dlist = the actualy display list which is be displayed by antic

dlisti = same display list but for every scanline DLI bit set!

dlistc = a shadow copy of the display list without DLI bits

dl_lookup = a lookup table containing the offsets to the right scanline in the displaylist as you have to consider the LMS commands which take 3 bytes in the display list & in the middle of the display list:

 


 org	dl_lookup

 dta 0,3,4,5,6,7,8,9

 dta 10,11,12,13,14,15,16,17

 dta 18,19,20,21,22,23,24,25

 dta 26,27,28,29,30,31,32,33

 dta 34,35,36,37,38,39,40,41

 dta 42,43,44,45,46,47,48,49

 dta 50,51,52,53,54,55,56,57

 dta 58,59,60,61,62,63,64,65

 dta 66,67,68,69,70,71,72,73

 dta 74,75,76,77,78,79,80,81

 dta 82,83,84,85,86,87,88,89

 dta 90,91,92,93,94,95,96,97

 dta 98,99,102,103,104,105,106,107

 dta 108,109,110,111,112,113,114,115

 dta 116,117,118,119,120,121,122,123

 dta 124,125,126,127,128,129,130,131

 dta 132,133,134,135,136,137,138,139

 dta 140,141,142,143,144,145,146,147

 dta 148,149,150,151,152,153,154,155

 dta 156,157,158,159,160,161,162,163

 dta 164,165,166,167,168,169,170,171

 dta 172,173,174,175,176,177,178,179

 dta 180,181,182,183,184,185,186,187

 dta 188,189,190,191,192,193,194,195

 dta 196,196,196,196,196,196,196,196

 dta 196,196,196,196,196,196,196,196

 dta 196,196,196,196,196,196,196,196

 dta 196,196,196,196,196,196,196,196

 dta 196,196,196,196,196,196,196,196

 

you can see the offset at scanline0 & 1 and at position

99 & 100.

 

remember that the display list looks very simple here

 

dlist $4e,screen_lo, screen_hi

$0e,$0e,$0e,$0e,$0e....

$4e,screen+offset_lo,screen+offset_hi

$0e,$0e,$0e,$0e,$0e....

$41,dlist

 

remember that for an antic e screen you need a second LMS command due to the 4k restriction of antic adress counter.

 

ok...the mapper is testing the sorted_y values if they are rejected sprites and if not sets the DLI bits. for 16 sprites maximum 4 DLIs are on screen at the moment. this sounds not optimal but at the moment it works better than setting DLIs for every sprite on screen.

 

after setting the DLIs the sprite data is copied to the player areas.

 


copy	mva #0	temp

mapper1	ldx temp

lda sortspry,x;get position where to copy data into player

beq	mapper3;=0 then it's a rejected sprite and will be skipped

mapper6	add #18    	

tay

lda maptabp,x;get which player sprite_no and #3

sta mapper2+2

mapper5	ldx #17	;18 bytes per sprite to copy incl. blank lines

mapper4	lda sprite,x

mapper2	sta p0,y+

dex

bpl mapper4

mapper3	inc temp  	;next sprite

lda temp

cmp #max_sprites

bcc mapper1

rts



maptabp	dta >p3,>p2,>p1,>p0

dta >p3,>p2,>p1,>p0

dta >p3,>p2,>p1,>p0

dta >p3,>p2,>p1,>p0

 

back to the example... sprite5 is rejected because when copying to the player area it would overwrite sprite1 data as they overlapp from scanline 14-18. ok this sounds fine...

 

so the mapper copies the sprite data in following order:

 

p3=sprite1

p2=sprite2

p1=sprite3

p0=sprite4

p3=sprite5 (but skipped due to overlapping with sprite1)

p2=sprite6

p1=sprite7

 

remember that the DLI bits are set at y-position of sprite1 and sprite6 (sprite4 is skipped as its rejected so next sprite will be start of the next DLI region)

 

ok.... for this frame everything is fine... but now let us assume the following...

 


1234.

....5

....6

....7

 

sprites 1,2,3,4 stay on their positions and sprite 5,6,7 are moving upwards starting f.e. from y-position 100...

 

everything is fine as long sprite5 is not causing trouble when trying to be on one scanlines containing 4 sprites... so as soon as it enters the zone of sprite 1,2,3,4 (zone range from scanline 10-17) it will be marked as rejected... and skipped by every other routine. remember the test:

 

for nr=4 to 7

if (sprite_y[nr]-sprite_y[nr-4])<8 then mark them as "to be rejected = sprite_y(nr-4)=0.

next nr

 

and NOW, emkay, here is the difficulty.... last frame there is sprite data from sprite1 in 10-17 of p3 and sprite data of sprite5 at 18-28.... and this has to be removed... but you might be right... as the sprite mapping order is always p3,p2,p1,p0,p3,p2,p1,p0 even if sprites are rejected...i could remove the "garbage" when overlapping by another DLI or by just writing 0 to the sprx table...

 

hmmm....i have to think...

 

ps. sorry for this long explanation...

Link to comment
Share on other sites

@ analmux

 

yes... this would be the next step... but at the moment if i put the "standard" method of sprite data handling in the engine it needs more than the available VBL time... or it nearly halfs the maximum sprite amount per frame...

 

my timepilot engine used this method with software sprites...

Link to comment
Share on other sites

 

or moving the sprite data in the PM memory(0 space 1 PM DATA) by simply copying always PM data including spaces?

 

"0111111110"

abcdefghijklmnop

01111111100000

00111111110000

00011111111000

 

The last method will need always two bytes more to handle, but you don't have to clear anything?!?!

 

That's useless when the stepsize of the movement is 2 or more.

F.e. stepsize 2 gives:

 

011111110000000

0101111111100000

0101011111111000

..etc.

 

 

In most cases we're not restricted to just stepsize 1.

 

 

Yes...ofcourse, when moving more than 1 y-position, you need more "Space" bytes.

The example was only to make my question more clear.

Link to comment
Share on other sites

just found this nice intro...

it shows 8 sprites moving around... but i am sure that this works just for the circle... ;) and look who has done this... Mr. RMT Raster...

 

Oh Yes! It's my intro for Flop magazine from the 1995 year! :)

There is inside really some easy "universal" system for PM multiplexing.

Subroutine is called with parameters X,Y,LETTERNUMBER(0-7) and it automatically discover the first free PM object in relevant Y range (from Y to Y+letter's height). If such free PM object (1-4) for this Y range was found, then it was used, if no, subroutine returned the zero value.

Of course, 4 objects at most can be in the same Y range at any time, that's why "MAGAZIN" letters are moving through the predefined circle. :)

Link to comment
Share on other sites

hmmm... raster... and how did you split up the players? how did you set the DLIs?

 

i mean your method seems quite faster than the "c64" routine i went down...

 

and you don't take antic e screen...

 

or is all precalculated for the circle??? xpos,ypos,dlipos?

 

have to check the disassembling...

Link to comment
Share on other sites

hmmm... raster... and how did you split up the players? how did you set the DLIs?

i mean your method seems quite faster than the "c64" routine i went down... and you don't take antic e screen...

or is all precalculated for the circle??? xpos,ypos,dlipos?

 

It's long ago... 9 years... but I think I remember, that there is xpos,ypos precalculated only.

DLI appears each 8 screen-lines (once for each character line), therefore Y ranges are explored in 8 points sections. It means four X-pos values for each 8 screenlines (and four values for color of this range). If X(i) pos for appropriate "8points" section is 0, is this PM in this section free for use, if nonzero, it is used => explore to next PM (i.e. X(i+1), ...).

Link to comment
Share on other sites

  • 5 years later...

I'm curious, since Heaven just pointed me at this thread from the war torn Atari vs CBM thread, how far it got with all of this multiplexing..

 

As I mentioned in the other thread I tried using 64 sprites like players and good results, except that it wasn't practical given I burnt nearly all the display time in a kernel repositioning sprites.. The aim being for some like 200+ sprites, it works, but wasn't practical, well not without a REU to speed up data transfers and sprite register loads, but that was not playing fair really.. I just wanted something that allowed me smaller vertically sized sprites than the regular 21 high for a bullet hell test I was working on.. Anyway.. I mentioned there I don't see why you can't have bucket loads of sprites on the A8 with your player setup ?

 

What's the problem ?

Surely you've just got to.

1) Sort sprites indices by Y..

2) Copy in sprite data to correct positions, allocating in round robin fashion..

3) At VBL, load 1st 4 sprite x-positions and colours..

4) And then repeatedly interrupt on the last line that the next sprite *was* used, so you can gain as much time as possible to set the sprite up.. Though there's 2 schools of thought here, interrupt early and cross your fingers you can set it up in time, or interrupt after the last time the new sprites actual hardware sprite was used.. I prefer the later..

 

If you're using variable height sprites as I liked to do in my mad setup ;)

Then you need to allocate the sprites in a sensible fashion, though I found there are surprising patterns to this which meant never searching more than one sprite to find a free slot, and then you have to do a 2nd sort on the end Y-positions of the sprites.. This is the key here, and using a Radix sort is perfect[1] in the 1st pass, but for the 2nd pass, the 'Ocean sort' (which I'm sure you've had off Lemon) is perfect for this.. Only a few sprites change end co-ordinates and this sort is ideal for it.. There's a much better version of it sitting on Codebase64, along with a demo 48 sprite multiplexer that show you how little time it really takes, bar the copying of course..

 

I'm on the verge of getting my hands dirty with this, but the A8 scares me ;) Too many new registers names and unfamiliar hardware...

 

As for intelligent flickering of sprites that I guess you'd need, I thought about this long and hard for what I was working on, and the best solution I came up with was no really practical given my choice of an radix sort.. The idea was to have the lower bit of a sprites Y represent if it was drawn last time or not, if it was the bit was set, so it would naturally end up higher up the sorted result indices.. The problem was this then meant I either needed to halve my y-resolution, or introduce more work into the Radix sort, which I wasn't prepared to do, but it would have been a very elegant solution I think :) No matter how many sprites you have on the same line, they'll just cycle in their native order with no overhead from the sprite code at all, well bar the extra code required to sort 9bits of position.... The 2nd tier sort wouldn't need to worry about that since it would only see sprites that have been deemed displayable anyway..I didn't get so far with that, because then I got distracted with the idea of using extra bit(s) to encode the priority of the sprite into the sort as well, which naturally led to a complete meltdown of the code..

 

But maybe you've got enough power to do this on the A8, I guess you probably have..

 

What exactly is stopping the A8 from doing lots of multiplexed sprites ? I thought this is something that it would excel in, given you've got a bucket load of additional CPU off the bottom of the screen without many cycles being eaten by the hardware..

I'm confused reading through this thread why the goal wasn't achieved ?

 

And I'm not looking for a fight here.. Honest :) This stuff genuinely interests me.. I was looking to make a big bullet hell game on the c64, but sadly it just won't cut it for what I want.. And don't you dare cut'n'paste that back into the other thread! :)

 

[1] I say perfect, but for the bullet hell style game it was where Y positions are going a fairly crazy each frame.. If your positions are fairly stable like most horizontal shooters, then the 'Ocean sort' is the best way, but needs keeping an eye on when the loads fluctuates rapidly, and in some cases using both is valid.. ie: using a metric to guesstimate which will be better, even something like running one cycle of the Ocean sort and seeing how many changes occur, then if its beyond some magic amount, Radix sort the lot.. It depends what you want.. I want a level performance.. I don't care if it's not the fastest, but I don't want it to budge from that if possible.. I don't want something that's blindly fast in 99.9% of cases and makes the game drop a frame in that other 0.1%..

 

Anyway, I'l shut up now :)

Bloody hell.. Sorry for the long post and dredging this up after 5 and a bit years of slumber, but Heaven pointed me here.. Blame him ;)

Edited by andym00
Link to comment
Share on other sites

but I have understood that you are gonna use 2 sorts... first radix and then ocean... ok. fair enough.

 

I never got into the flickering ("ring buffer approach") as I was little bit disappointed that even my small example (not 100% optimised of course) eat such many cycles...

 

but my idea was mainly

 

- radix sort

- set DLI for the first zone (based on the first 4 sprites)

- copy the 4 virtual sprite data into the 4 hardware sprites P0,P1,P2,P3

- set DLI for the 2nd zone (based on the next 4 sprites)

- copy the 4 virtual sprite data into the 4 hardware sprites P0,P1,P2,P3

 

 

...

Link to comment
Share on other sites

but I have understood that you are gonna use 2 sorts... first radix and then ocean... ok. fair enough.

 

I never got into the flickering ("ring buffer approach") as I was little bit disappointed that even my small example (not 100% optimised of course) eat such many cycles...

 

but my idea was mainly

 

- radix sort

- set DLI for the first zone (based on the first 4 sprites)

- copy the 4 virtual sprite data into the 4 hardware sprites P0,P1,P2,P3

- set DLI for the 2nd zone (based on the next 4 sprites)

- copy the 4 virtual sprite data into the 4 hardware sprites P0,P1,P2,P3

 

 

...

 

Regards the speed issue, surely you're just looking at 8 cycles per player line to copy in, and 4 cycles per line to erase ? That's surely just (including some overhead) about one scanline per sprite ?

 

And it that's too much, move the erasing into the display portion of the screen and erase the sprites in the plexor interrupts ? You know which sprite you've just finished displaying, so after you've set the new player position and colour up, ack your interrupts and then start the erasing of the sprite data and let the plexor interrupt re-enter if you're too slow.. Then clearing sprites doesn't have to be done in the VBL, which'll save you a bit of time.. If you're sprite code is jumping to IRQs for each different hardware player, then clearing is dead simply since you now exactly which strip you need to clear, so it just load an index, load zero, stomp stomp stomp :)

 

Have each virtual sprite have its own interrupt handler where it knows which sprite it's going to load, then have it update the lo-byte (occasionally the hi-byte) of the interrupt vector as it progresses through to the next one, as I said above, makes the clearing a bit quicker since you don't have to muck about branching off to the correct unrolled code to clear, since you know you're about to erase player 0 data..

 

As for 2 sorts, I only use 2 sorts when there's variable sprite heights in use, since the allocation of sprites is no longer just 1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8,etc.....

And I wanted to be able to have a sorted list of where the sprites end.. I also use this on more traditional plexers, where you want to glue together multiple sprites without having to have scanline perfect IRQs, and then use the overlapped graphics technique to give you a few scanlines (~5 or so) grace to glue them together.. This means you can then have nice big big sprites on screen, along with lots of little ones very little cost, bar the secondary sort, and the additional handling of a parent link..

 

And I'd really say don't use 'zones' they will make life hell as you try to work around them when things are not so under your control ;)

When you want to load a new sprite, interrupt on the line it was last used, load the sprite, and then check if the next sprite is ready to load in the same interrupt, if so do it ;)

 

I wrote many multiplexers based on 'zones' back on the 80s and everyone of them was flawed in some way, I don't know why it took me so many attempts before I realised that they're just absolutely crap.. Maybe because they seemed easier to manage.. They just never work perfectly, always letting you down in some fashion when you least want it..

 

Interrupting a few lines early, or on the last line of its last use always works better..

 

The flickering stuff would work beautifully if you use a 9bit sort key which a 2 pass radix with 4 and 5 bits would do very nicely.. Then you don't even have to worry about it.. Just once you've displayed a sprite in the interrupt, you set it's displayed flag, which is then picked up next time round in the key generation phase as you sort.. You don't even have to take it into account since they'll naturally sit higher in the sort results than the undisplayed sprites.. I wouldn't even call it a ring-buffer since the right results jsut naturally fall out of the system..

Though on the A8, I'd think some other means would be better, rather than simply not displaying an entire sprite, display what you can of it before you have to cut it off, again having priorities in the sort key is probably beneficial to doing that..

 

Edit: Oh, and one more thing.. The main advantage to having a seperate IRQ for each virtual sprite is that you can pull out all the data you need to reload, and have it buffered in the irq code itself, meaning you're no longer tied to having to try and run the sprite processing logic in the vbl, or trying to use messy double buffered schemes.. The game code sees one constant set of sprite arrays, the multiplexor code as well, and best of all you can run your logic during the main display, not trying to keep it running inside the small VBL time.. The data stuffing pass is dead quick as well, especially since you'll only be touching an x-position and colour..

 

One more thing.. Where's the best tut. for getting started on the A8s.. I want to use my favourite assembler and carry on using DevStudio, so is there some simple way I can create Atari executables from a binary ? I mean some magic data and addresses I need to insert at the start of my bin ? I'm useless here.. I don't know about running stuff on Ataris.. I'm hoping it's going to be something like the 64.. It's not is it ?

Edited by andym00
Link to comment
Share on other sites

and how do you handle when >1 sprites are starting on 1 scanline?

Edit: Sorry misread.. Since you've entered the IRQ on the last line of the player that was last used for this sprite, you just cross your fingers and hope you're going to get it done in time..

Obviously the catastrophic case is 8 sprites, seperated by one line, then 8 more sprites.. Not much you can do in that case.. I didn't use the not seperated case because then I assume you'd be building big glued together sprites, and that's best done other ways..

No plexer can handle *everything*..

But a common solution I use, is where you'd have that scenario, you offset the sprites by 1 in the Y.. Even in a big 4x4 sprite you'd still offset each sprite column by one in the Y to avoid exactly what you've suggested..

The 64 can handle a lot of that problems, since the Ys can be reloaded before the old sprite has finished, and sprite pointers can be double buffered since they're the top 8 bytes of the screen memory.. But on the A8 you've got less to reload, and the Y-position can't be missed, so even if you can't fall through the 4 sprite handlers in time the worst case is that the x position is wrong for one line, or a colour is wrong for one line..

The plexer isn't going to magically fix everything since you've still got a finite amount of time to reload registers and only a small window to do so in, but you can minimise those effects with care..

Edited by andym00
Link to comment
Share on other sites

sounds interesting with one interrupt per sprite...

 

have to think about... too many projects at the moment... private things, Gridrunner, Beyond Evil, WoW, Trials HD etc... ;)

 

Well I've got some time.. How do I make Atari executables from binaries ? What magic data do I need to put in to make it runnable ?

And what's the best emulator right now ?

I never thought I'd see this day ;)

Link to comment
Share on other sites

atari executables have following binary header

 

$ff,$ff

lo,hi byte of starting adress in memory

lo,hi byte of endinf adress-1

 

and end of your code you are putting

 

$e0,$02

lo, hi run adress

 

$2e0,$2e1 tells DOS or nearly all boot loaders where the code should start

 

(just a small introduction to file format of A8)

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...