Jump to content

About This Club

Atari VCS/2600 development using the Harmony/Melody board. See the GENERAL discussion area for setting up your environment. See the CDFJ discussion area for the tutorial.

  1. What's new in this club
  2. I thought I'd start a collection of useful tidbits. If you know of something please post it and I'll add it to this list. Hardware Division via Multiplication - the ARM doesn't support division via hardware, but we can use this trick to quickly divide by a fixed value.
  3. In Frantic I draw the playfield using datastreams with a 0.20 increment, which repeats the playfield data over 5 scanlines. All collision processing in Frantic is done using software, so to determine collisions with the playfield I needed to divide the Y position by 5. The ARM used in the Harmony/Melody does not have support for division, but it does support multiplication. Most of you are probably familiar with fractional or subpixel positioning the moveable objects in the 2600, and we can use the same idea to implement hardware division by using what's known as Reciprocal Multiplication. Basically Y / 5 is the same as Y * 0.20 , and we can implement that by allocating the lower X bits of the 32 bit value as the fractional value. The article I linked to has this handy chart: n scaled reciprocal shift count 3 1010101010101011 AAAB 17 5 1100110011001101 CCCD 18 6 1010101010101011 AAAB 18 7 10010010010010011 19 9 1110001110001111 E38F 19 10 1100110011001101 CCCD 19 11 1011101000101111 BA2F 19 12 1010101010101011 AAAB 19 13 1001110110001010 9D8A 19 14 10010010010010011 20 15 1000100010001001 8889 19 Which shows us that to divide by 5 we need to use the lower 18 bits as the fractional value, and that 0.20 works out to be 0xCCCD. #define DIV_BY_5 0xCCCD #define DIV_BY_5_SHIFT 18 y5 = (y * DIV_BY_5) >> DIV_BY_5_SHIFT; You'll notice the list skips a number of values - for those we can use bit shifting to divide by powers of 2: result = value >> 1; // divide by 2 result = value >> 2; // divide by 4 result = value >> 3; // divide by 8 result = value >> 4; // divide by 16 The table is based on needing to divide a 16 bit value, which does not work for /7 or /14. I haven't looked into it, but suspect they'd work just fine to divide a Y position which is an 8 bit value. The article does go into the extra steps needed to divide a 16 bit value by 7 or 14.
  4. Ahh... excellent, I get it. Thanks for the response. I can never get all the way through the instructions before beginning to play with code or a new device! 😃
  5. You're getting ahead of the tutorial Display Data RAM is not mapped into 6507 address space so you cannot directly load, store, increment, decrement, etc. the value. You would: put MyVar: ds 1 as part of the Zero Page RAM put _MYVAR: ds 1 as part of Display Data RAM update CallArmCode to copy MyVar into _MYVAR Then your 6507 code would use MyVar. In Part 8 the 6502 variables TimeLeftOS and TimeLeftVB are added and get passed to the ARM code.
  6. Darrell, I'm a bit confused on how the 6502 passes data to the ARM processor. CDFJ seems to recognize registers such as SWCHA and INPUT4 - etc. when identified like so: _SWCHA: ds 1 ; joystick directions to ARM code .. but what if I want to define a variable in 6507 ASM code, increment the variable value throughout the Kernel, and then make the final value available to the ARM processor in overscan? With the understanding that I would still need to include it in the "CallArmCode:" routine in order to pass it to the ARM, would still I declare the variable in the same location as _SWCHA in the same manner like so: _MYVAR: ds 1 ;a variable that is not a register ...could I then use that variable as I normally would in a pure ASM program, like so: ldx #5 stx _MYVAR inc _MYVAR
  7. Display Data RAM is defined as this: void* DDR = (void*)0x40000800; #define RAM ((unsigned char*)DDR) #define RAM_INT ((unsigned int*)DDR) #define RAM_SINT ((unsigned short int*)DDR) So the memory accessed when using these defines is: RAM[0] is 1 byte at 0x40000800 RAM[1] is 1 byte at 0x40000801 RAM[2] is 1 byte at 0x40000802 RAM[3] is 1 byte at 0x40000803 ... RAM_INT[0] is 4 bytes starting at 0x40000800 RAM_INT[1] is 4 bytes starting at 0x40000804 RAM_INT[2] is 4 bytes starting at 0x40000808 RAM_INT[3] is 4 bytes starting at 0x4000080C ... RAM_SINT[0] is 2 bytes starting at 0x40000800 RAM_SINT[1] is 2 bytes starting at 0x40000802 RAM_SINT[2] is 2 bytes starting at 0x40000804 RAM_SINT[3] is 2 bytes starting at 0x40000806 ... From dasm we get the location of _BUF_JUMP1 and _BUF_JUMP1_EXIT as an offset in bytes from RAM, so we need to divide the offset by 2 for RAM_SINT. If we're using RAM_INT we need to divide the offset by 4. An example of that is also in InitGameBuffers(): // Zero out the buffers used to hold the player, missile, and ball data // It's fastest to use myMemsetInt, but requires // proper alignment of the data streams (the ALIGN 4 pseudops found in the // 6507 code). Additionally the offset(_GameZeroOutStart) and // byte count(_GameZeroOutBytes) must both be divided by 4. myMemsetInt(RAM_INT + _EVERY_FRAME_ZERO_START/4, 0, _EVERY_FRAME_ZERO_COUNT/4); This is one of those things that's easy to miss - Dionoid pointed out one I missed in Part 5.
  8. Is that correct - signed int values are 4 bytes (32-bit) in length, while the jump addresses are 2 bytes (16 bits)? EDIT: I see #define RAM_SINT ((unsigned short int*)DDR), so they are indeed 16 bit, but I'm still unsure why the /2 is needed? Chris
  9. Added the arena Source Code Download and unzip this in your shared directory Collect3_20200712.zip ROM for reference collect3_20200712.bin Arena The arena is drawn using the increment feature of the data streams. Usually we use an increment of 1.0 but we can use smaller increments like 0.25, which will return a value from the data stream 4 times before advancing to the next value. The fractional portion of the increment is set using a byte value, so if we wanted to set 0.25 we'll times the .25 of it by 256 for 64, then we pass the values 0 and 64 like this: setIncrement(_DS_PF0_LEFT,0,64); // set the Data Stream increment to 0.25 To calculate out the fractional increment needed to stretch an arbitrary image of a certain height over a set of scanlines use: 256 * height / scanlines We'll let dasm calculate that for us: _ARENA_INCREMENTS: .byte 256 * Arena1_Height / _ARENA_SCANLINES .byte 256 * Arena2_Height / _ARENA_SCANLINES .byte 256 * Arena3_Height / _ARENA_SCANLINES .byte 256 * Arena4_Height / _ARENA_SCANLINES While we need 6 bytes of data to send to the playfield registers, we'll define the graphics using just 5 bytes per row like this: As mentioned before, the ARM has an inline barrel shifter which makes it super fast to shift bit values around, we'll utilize those to break the 5 bytes apart into the 6 bytes needed for TIA: void PrepArenaBuffers() { // This function loads the selected Arena layout into the 6 playfield buffers. // // The 40 bits for each row of arena data are stored in 5 bytes arranged like this: // byte 0 byte 1 byte 2 byte 3 byte 4 // 33333333 33222222 22221111 11111100 00000000 // 98765432 10987654 32109876 54321098 76543210 // // They need to be converted to this arrangement for the playfield datastreams: // LEFT RIGHT // PF0 PF1 PF2 PF0 PF1 PF2 // 3333---- 33333322 22222222 1111---- 11111100 00000000 // 6789---- 54321098 01234567 6789---- 54321098 01234567 int row; unsigned char byte0, byte1, byte2, byte3, byte4; unsigned char *arena = ROM + arena_graphics[mm_arena]; unsigned char *arena_pf0_left = RAM + _BUF_PF0_LEFT; unsigned char *arena_pf1_left = RAM + _BUF_PF1_LEFT; unsigned char *arena_pf2_left = RAM + _BUF_PF2_LEFT; unsigned char *arena_pf0_right = RAM + _BUF_PF0_RIGHT; unsigned char *arena_pf1_right = RAM + _BUF_PF1_RIGHT; unsigned char *arena_pf2_right = RAM + _BUF_PF2_RIGHT; for(row=0; row<arena_heights[mm_arena]; row++) { // fetch the 5 bytes for the current row byte0 = arena[row*5 + 0]; byte1 = arena[row*5 + 1]; byte2 = arena[row*5 + 2]; byte3 = arena[row*5 + 3]; byte4 = arena[row*5 + 4]; // convert the 5 bytes into the 6 needed for TIA's PFx registers arena_pf0_left[row] = BitReversal(byte0) << 4; arena_pf1_left[row] = (byte0 << 4) + (byte1 >> 4); arena_pf2_left[row] = BitReversal((byte1 << 4) + (byte2 >> 4)); arena_pf0_right[row] = BitReversal(byte2); arena_pf1_right[row] = byte3; arena_pf2_right[row] = BitReversal(byte4); } // set the color of the arena ARENA_COLOR = ColorConvert(arena_color[mm_arena]); } With a large number of arenas this will end up saving ROM. In C use << to shift left and >> to shift right. The number after the << or >> denotes how many bits to shift. So value << 4 is equivalent to 6507 code of: lda value asl asl asl asl and value >> 4 is equivalent to: lda value lsr lsr lsr lsr The helper function BitReversal takes care of the bit order needed for PF0 and PF2: unsigned int BitReversal(unsigned int value) { // value a byte with bits in the order 76543210 // return a byte with bits in the order 01234567 value = ((0xaa & value) >> 1) | ((0x55 & value) << 1); value = ((0xcc & value) >> 2) | ((0x33 & value) << 2); value = ((0xf0 & value) >> 4) | ((0x0f & value) << 4); return value; } Since we're using this function to reverse a byte you'd expect the function to be defined as unsigned char, but one of the things about the ARM is it's more efficient to use 32 bit values when possible - it results is smaller code, and faster execution. Fast Jump Fast Jump is one of the really slick features in CDFJ. It came about because of a suggestion by @ZackAttack: What this does is eliminate the use of DEY (or DEX) and BNE for the kernel loop control, and replaces it with a data stream filled with 2 byte addresses and jmp $0000. While it's only a 2 cycle savings per loop it allows us to jump to many different kernels without any 6507 overhead, which will be very useful when we add support to reposition the players mid-screen. The Kernel currently looks like this: _NORMAL_KERNEL: sta WSYNC ;--------------------------------------- lda #_DS_GRP1 ; 2 2 values from datastream pointing at _BUF_PLAYER1 sta GRP1 ; 3 3 lda #_DS_COLUP0 ; 2 7 values from datastream pointing at _BUF_COLOR0 sta COLUP0 ; 3 10 lda #_DS_COLUP1 ; 2 12 values from datastream pointing at _BUF_COLOR1 sta COLUP1 ; 3 15 lda #_DS_PF0_LEFT ; 2 17 values from datastream pointing at _BUF_PF0_LEFT sta PF0 ; 3 20 PF0L 55-22 lda #_DS_PF1_LEFT ; 2 22 values from datastream pointing at _BUF_PF1_LEFT sta PF1 ; 3 25 PF1L 66-28 lda #_DS_PF2_LEFT ; 2 27 values from datastream pointing at _BUF_PF2_LEFT sta PF2 ; 3 30 PF2L before 38 lda #_DS_GRP0 ; 2 32 values from datastream pointing at _BUF_PLAYER0 sta GRP0 ; 3 35 VDELP0 is on, so this is for next scanline lda #_DS_PF0_RIGHT ; 2 37 values from datastream pointing at _BUF_PF0_RIGHT sta PF0 ; 3 40 PF0R 28-49 lda #_DS_PF1_RIGHT ; 2 42 values from datastream pointing at _BUF_PF1_RIGHT sta PF1 ; 3 45 PF1R 39-54 lda #_DS_PF2_RIGHT ; 2 47 values from datastream pointing at _BUF_PF2_RIGHT sta PF2 ; 3 50 PF2R 50-65 jmp FASTJMP1 ; 3 53 addresses from datastream pointing at _BUF_JUMP1 _EXIT_KERNEL: ; 53 sta WSYNC ; ldx #0 ; 2 2 stx GRP0 ; 3 5 stx GRP1 ; 3 7 stx PF0 ; 3 10 stx PF1 ; 3 13 stx PF2 ; 3 16 Since we're dealing with addresses we need to use 2 byte values in the buffers. We also need to make sure they're 2 byte aligned as required by the ARM processor: align 2 ; jump addresses are word values, so must be 2 byte aligned for the ARM code _BUF_JUMP1: ds _ARENA_SCANLINES * 2 _BUF_JUMP1_EXIT: ds 2 The C code will fill _BUF_JUMP1 with the value _NORMAL_KERNEL and _BUF_JUMP1_EXIT with _EXIT_KERNEL: // set the Jump Datastream so each entry runs the NORMAL KERNEL by default // init Jump Datastream for(i=0;i<_ARENA_SCANLINES;i++) RAM_SINT[(_BUF_JUMP1 / 2) + i] = _NORMAL_KERNEL; RAM_SINT[ _BUF_JUMP1_EXIT / 2 ] = _EXIT_KERNEL; One thing to be aware of is RAM_SINT references the RAM as signed integer values, which are 2 bytes in size, while the offsets of _BUF_JUMP1 and _BUF_JUMP1_EXIT are based on byte values. So we must divide them by 2 when using RAM_SINT.
  10. Program logic for the next update is finished. I'm now doing a review of the code to revise the comments, such as this: Mode: ds 1 ; $00 = splash, $01 = menu, $80 = game became this: Mode: ds 1 ; $00 = splash, $01 = menu, $80 = game ; these values allow for easy testing of Mode: ; LDA Mode ; BMI GAME_ROUTINE ; BNE MENU_ROUTINE ; BEQ SPLASH_routine and revise variable names such as: _ARENA_HEIGHT = 182 became this: _ARENA_SCANLINES = 180 ; number of scanlines for the arena because the original name could easily be confused with the new heights table for the arenas: _ARENA_HEIGHTS: .byte Arena1_Height .byte Arena2_Height .byte Arena3_Height .byte Arena4_Height The 6507 review is finished, and am about half way done with the C review. I hope to get the next entry posted this weekend.
  11. After a long break, I'm finally getting back into 2600 development. Been working on Part 9 - Arena. It shows off how to use the incremental data streams as well as a jump data stream for program flow control. You may have noticed the playfield data is using 5 bytes per row instead of the usual 6. I'm showing how to convert that to the 6 bytes needed for the playfield registers. I used a slightly more advanced version of this in Stay Frosty 2, which allowed for scrolling levels of arbitrary widths. void PrepArenaBuffers() { // The 40 bits for each row of arena data are stored in 5 bytes arranged like this: // byte 0 byte 1 byte 2 byte 3 byte 4 // 33333333 33222222 22221111 11111100 00000000 // 98765432 10987654 32109876 54321098 76543210 // // They need to be converted to this arrangement for the playfield datastreams: // LEFT RIGHT // PF0 PF1 PF2 PF0 PF1 PF2 // 3333---- 33333322 22222222 1111---- 11111100 00000000 // 6789---- 54321098 01234567 6789---- 54321098 01234567 int r; unsigned char *arena = ROM + arena_graphics[mm_arena]; unsigned char *arena_pf0_left = RAM + _BUF_PF0_LEFT; unsigned char *arena_pf1_left = RAM + _BUF_PF1_LEFT; unsigned char *arena_pf2_left = RAM + _BUF_PF2_LEFT; unsigned char *arena_pf0_right = RAM + _BUF_PF0_RIGHT; unsigned char *arena_pf1_right = RAM + _BUF_PF1_RIGHT; unsigned char *arena_pf2_right = RAM + _BUF_PF2_RIGHT; for(r=0; r<arena_heights[mm_arena]; r++) { arena_pf0_left[r] = BitReversal(arena[r*5 + 0]) << 4; arena_pf1_left[r] = (arena[r*5 + 0] << 4) + (arena[r*5 + 1] >> 4); arena_pf2_left[r] = BitReversal((arena[r*5 + 1] << 4) + (arena[r*5 + 2] >> 4)); arena_pf0_right[r] = BitReversal(arena[r*5 + 2]); arena_pf1_right[r] = arena[r*5 + 3]; arena_pf2_right[r] = BitReversal(arena[r*5 + 4]); } }
  12. The missing elements are still there. Just increase the debugger's screen size in the developer options.
  13. It would be a useful feature if Stella allowed you to highlight 1, 2, or 4 bytes in the Cartridge Ram, and it displayed the the result in MSB order for both hex and decimal. The debugger has also changed format with the latest release so it is shorter in height, and the "label", "dec", and "bin" are gone on the cartridge ram. However I think something like I mocked below could work. What do you guys think? I suppose the length of the box for decimal would need to increase, but that shouldn't be a show stopper.
  14. I thought the break would be a week max, turned out to be a month. The last feature request is quite nice, I should have thought of it myself - you can now start a new game of Kaboom! Deluxe! via the paddle button. Anyway, this weekend I plan to review where I left off on the CDFJ tutorial, then start work on Part 9 - Arena.
  15. Saw something today that reminded me of my unfinished Kaboom! Deluxe! hack. There's not much left to do in it, so I think I'll take a brief break from CDFJ and see if I can't finish the hack this weekend.
  16. Added score & timer Source Code Download and unzip this in your shared directory. Collect3_20200211.zip ROM for reference collect3_20200211.bin Score & Timer I like to implement routines for displaying the score early on in a project - you can see that in these early builds of Collect, Frantic, Medieval Mayhem, and Space Rocks. Even though I'm not ready to show the score, the display is very useful for showing diagnostic information such as in this build of Frantic were I used the score to display the results of the software sprite collision1 routines. One of the challenges when developing DPC+ARM code is that Stella does not emulate how long ARM code takes to run. As far as it's concerned, all ARM code will finish executing in 0 cycles of 6507 time. Because of this it's very easy to write something that will run just fine in emulation, but will cause screen jitters and/or rolls, or even a fatal crash when run on a real Atari. We're already checking timers in our 6507 code, so we can easily save those values and display them in the score. For the game we need to display two scores and a timer. In the 2K version of Collect I used the playfield to show that information. For Collect 3 we're going to do this: players are set to 3 copies with medium spacing players are positioned so the middle copies occupy the middle 16 pixels of the display playfield pixels are turned on behind all copies of the players just like the ShowTwoColorGraphic routine used to display the menu, the players are colored black and used as stencils playfield color is changed on the fly so each group of 16 pixels has a different color use a 3 pixel font, and 1 pixel spacing, so we can display 4 digit scores for each player For diagnostic purposes the difficult switches are used to control what is displayed: Left B and Right B - Score and Timer for the game Left A and Right B - Test Pattern Left B and Right A - Timing Remaining in Vertical Blank and Overscan Left A and Right A - sprite X Y positions The test pattern shows an improvement over the prior version of the score & timer kernel that I used for the unfinished DPC+ tutorial: Previously the update of player1 for the right player's score was 1 pixel too late, resulting in bit 7 of the timer being shown. I worked around it then by designing the font to not use bit 7. PrepScoreDatastreams() A new function was added to prepare the datastreams for the score kernel. Since we're using a skinny font, each datastream will contain graphics for 2 digits. MergeCharacters() handles this: void MergeCharacters(int datastream, int left_character, int right_character) { int i; unsigned char *stream = RAM + datastream; unsigned char *left = ROM + _FONT + left_character * _FONT_HEIGHT; unsigned char *right = ROM + _FONT + right_character * _FONT_HEIGHT; for (i=0;i<_FONT_HEIGHT;i++) *stream++ = ((*left++) & 0xf0) + ((*right++) & 0x0f); } The character values of 0-15 correspond to graphics of 0-9 and A-F so that hex values can be shown using simple masking and/or bit shifting. These two calls prep the right score to show 4 hex digits of the frame counter: MergeCharacters(_BUF_SCORE1_A, (frame >> 12) & 0x0f, (frame >> 8) & 0x0f); MergeCharacters(_BUF_SCORE1_B, (frame >> 4) & 0x0f, frame & 0x0f); This also means we'll be using binary-coded decimal (BCD) for the score, which you should already be familiar with from writing 6507 code. Character values 16 and above are used for special characters, such as the colon used to show 2:00 in the timer display. MergeCharacters(_BUF_TIMERA, 2, 17); MergeCharacters(_BUF_TIMERB, 0, 0); Constants are defined for these to make the source code easier to understand: MergeCharacters(_BUF_TIMERA, 2, _FONT_COLON); MergeCharacters(_BUF_TIMERB, 0, 0); The 6507 does not support hardware multiplication, so score fonts tend to be designed to use 8 scanlines. This is because times 8 can be calculated using 3 ASL commands. The ARM does support hardware multiplication, so the font for Collect3 uses 7 scanlines to show we have that option. Do note the ARM in the Harmony/Melody does not support hardware division. 1 TIA's hardware collision registers are not very helpful when multiplexing a multitude of sprite images through TIA's two players, some of the images might never be drawn on the same frame, so collision detection must be handled in software.
  17. I tweaked the playfield values for the menu to better show how the second color works. This will be in the source when I post Part 8. _MENU_CONTROL: .byte %00000000 ; PF0 .byte %01000000 ; PF1 .byte %00000000 ; PF2 .byte 100 ; ball X location .byte _MENU_LOGO_HEIGHT ; rows .byte MM_TITLE_GAP ; extra scanlines _MENU_FIRST_OPTION_ID = (* - _MENU_CONTROL) / 6 _MENU_PLAYERS_ID = (* - _MENU_CONTROL) / 6 .byte %00000000 ; PF0 .byte %01000000 ; PF1 .byte %00000000 ; PF2 .byte 0 ; ball X location .byte _MM_OPTION_HEIGHT ; rows .byte MM_OPTION_GAP ; extra scanlines _MENU_ARENA_ID = (* - _MENU_CONTROL) / 6 .byte %00000000 ; PF0 .byte %01000000 ; PF1 .byte %00000000 ; PF2 .byte 0 ; ball X location .byte _MM_OPTION_HEIGHT ; rows .byte MM_OPTION_GAP ; extra scanlines _MENU_OPTION1_ID = (* - _MENU_CONTROL) / 6 .byte %00000000 ; PF0 .byte %11000000 ; PF1 .byte %00000000 ; PF2 .byte 0 ; ball X location .byte _MM_OPTION_HEIGHT ; rows .byte MM_OPTION_GAP ; extra scanlines _MENU_OPTION2_ID = (* - _MENU_CONTROL) / 6 .byte %00000000 ; PF0 .byte %11000000 ; PF1 .byte %00000000 ; PF2 .byte 0 ; ball X location .byte _MM_OPTION_HEIGHT ; rows .byte MM_OPTION_GAP ; extra scanlines _MENU_TV_TYPE_ID = (* - _MENU_CONTROL) / 6 .byte %11100000 ; PF0 .byte %11000000 ; PF1 .byte %00000000 ; PF2 .byte 0 ; ball X location .byte _MM_OPTION_HEIGHT ; rows .byte MM_START_GAP ; extra scanlines _MENU_START_ID = (* - _MENU_CONTROL) / 6 .byte %00000000 ; PF0 .byte %00000000 ; PF1 .byte %00000000 ; PF2 .byte 0 ; ball X location .byte _MM_OPTION_HEIGHT ; rows .byte MM_END ; extra scanlines _MENU_CONTROL_SIZE = * - _MENU_CONTROL Players, Arena, Option 1 and Option 2 now use less playfield for the second color. TV Type was unchanged as SECAM needs that extra playfield pixel. Missiles disabled to better show playfield usage.
  18. Added menu Source Code Download and unzip this in your shared directory. Collect3_20200208.zip ROM for reference collect3_20200208.bin Menu Implemented a menu using the 2-color 48 pixel kernel. Menu is fully functional, though Option 1 and 2 don't affect anything except the display of a check or X. Selected values for Players and Arena will affect the display of the game screen. Players 1, Arena 1 Only 1 player in a black arena Players 2, Arena 2 2 players in a red arena When implementing this I had to move the layout of Display Data to the end of collect3.asm to prevent label mismatch errors: dasm collect3.asm -f3 -ocollect3.bin -lcollect3.lst -scollect3.sym collect3.asm (215): error: Label mismatch... --> _BUF_MENU_CONTROL 000c collect3.asm (216): error: Label mismatch... --> _BUF_MENU_COLORS 000c 48 pixel 2 color kernel The 48 pixel kernel was developed for Stay Frosty 2. It works by using the players as stencils: The players are colored black, so all the 1 bits will be black while all the 0 bits will show through the underlying colors of the background and playfield. The playfield and background colors is set to black for the left side of the screen, changed just before the 48 pixel display begins, then changed back to black for the right side of the screen. The color change doesn't happen at exactly the right time, so the missiles are used to hide the transition. You can see that by using Stella's Developer Keys to toggle both missiles (ALT + C and ALT + V for Linux or Windows, COMMAD + C and COMMAND + V for Mac): Toggling Fixed Debug Colors mode (ALT + COMMA or COMMAND + COMMA) will reveal the layout (be sure to toggle the missiles back on!): The playfield is limited to coloring 4 adjacent pixels at a time, but you can use the ball to fine-tune where the second color shows up - you can see it used for the 3 in COLLECT 3. The playfield can be changed for each entry. This menu doesn't show it off to great effect as just the game title uses a different playfield layout. Some new constants were added to control the spacing in the menu. Whenever you modify the menu make sure to also modify these constants to maintain the 262 scanline count. ; controls spacing in main menu MM_TITLE_GAP = 20 ; gap after game title MM_OPTION_GAP = 8 ; gap between options MM_START_GAP = 20 ; gab before START option MM_LOGO_GAP = 20 ; gap before company logo MM_END = $80 _MM_OPTION_HEIGHT = 7 Only the constant for Option Height is directly used by the C code, so it's prefixed with the _. There are 3 datastreams used by the menu. ; datastream usage for Menu _DS_MENU_GRAPHICS = DS0DATA _DS_MENU_CONTROL = DS1DATA _DS_MENU_COLORS = DS2DATA The graphics datastream contains the graphics as shown on the screen, with an initial value set for each option. The value will be overwritten if the value is different than the initial value. The control datastream holds the playfield pattern used for each entry, the position of the ball (set to 0 if not using it), the height of the option, and the number of extra scanlines to show after the option. Setting extra scanlines to $80 triggers the end of the menu. _MENU_CONTROL: .byte %00000000 ; PF0 .byte %01000000 ; PF1 .byte %00000000 ; PF2 .byte 100 ; ball X location .byte _MENU_LOGO_HEIGHT ; rows .byte MM_TITLE_GAP ; extra scanlines _MENU_FIRST_OPTION_ID = (* - _MENU_CONTROL) / 6 _MENU_PLAYERS_ID = (* - _MENU_CONTROL) / 6 .byte %11100000 ; PF0 .byte %11000000 ; PF1 .byte %00000000 ; PF2 .byte 0 ; ball X location .byte _MM_OPTION_HEIGHT ; rows .byte MM_OPTION_GAP ; extra scanlines ... _MENU_START_ID = (* - _MENU_CONTROL) / 6 .byte %00000000 ; PF0 .byte %00000000 ; PF1 .byte %00000000 ; PF2 .byte 0 ; ball X location .byte _MM_OPTION_HEIGHT ; rows .byte MM_END ; extra scanlines The SpiceWare logo is not part of menu control as it uses a different kernel to extend the lines left and right. In the C code MenuOverScan() has been updated to call new function ProcessMenuJoystick() - but only if the user hasn't pressed GAME RESET on the console. MenuVerticalBlank() has been updated to do a mass-copy that sets default values for the menu's graphics, control, and colors datastreams. The colors are then updated for NTSC/PAL/SECAM by using ColorConvert(). Next menu options are updated if they need to show a different graphic than the default one. Lastly one of the colors for the selected option will be flashed using the frame counter. C data in 6507 source For most of my projects I've only used a single 4K bank for 6507 code. It usually ends up quite full, so data that's not directly accessed by the 6507 is put either in the C code, or after the compiled ARM code. An example of data in the C code is the color table used to test the new Arena menu option: const unsigned char arena_color[4] = { _BLACK, _RED, _GREEN, _BLUE }; Graphic data can also be stored in the C code, though I had 2 conflicts with doing so. I already have tools that convert images for the format used by dasm. Lists of images use up twice as much ROM when stored in C code as it does when stored in the 6507 code. #1 is a minor issue. #2 can be a major issue, even with a 32K game. As an example Draconian has 136 images in the table which takes up 272 bytes in the 6507 code. If the table was in the C code it would have taken 544 bytes. Because of this I've taken to putting the graphics and related data (like the menu control) in the 6507 source file. It's located after the ARM code, and before the banks of 6507 code. There is something to be aware of when doing this - because the build process is this: assemble 6507 code to create defines_from_dasm_for_c.h compile C code to create ARM routines assemble 6507 to create final ROM Step 1 uses the previously compiled ARM code, which will likely change size in step 2. This can make things move in the ROM when step 3 is done. So the data is broken up into 2 groups, ARM Indirect Data and ARM Direct Data. ARM Indirect Data is data the C code doesn't directly access. You can tell it's not accessed because the labels are not prefixed with a _ character, so they don't end up in defines_from_dasm_for_c.h: ARM Direct Data is directly accessed by the C code. We use an ORG to prevent it from shifting around during the 3 steps of the build process. You'll notice the graphic images of PLAYER_LEFT and PLAYER_RIGHT seen above are in the table _IMAGE_GRAPHICS seen below. The C code uses this table to access the graphics. The ORG value will need to be adjusted as the project is developed.
  19. The datastream for audio is AMPLITUDE: lda #AMPLITUDE sta AUDV0 However, it's a little different from the other datastreams. Instead of reading from a buffer you populated in Display Data RAM, it will either: Retrieve value from a ROM buffer* that's packed with 2 samples per byte (speech in Draconian) Created value on the fly for 3 voice music (music in Mappy) I have a music example somewhere, will track it down and get it to you. In Collect 3 the ARM code is triggered via: ldx #$FF stx CALLFN Change it to this to have AUDV0 updated once per scanline while the ARM code is running. ldx #$FE stx CALLFN * I think it might even work if the buffer is in RAM, which would allow you to manipulate them.
  20. Hi @SpiceWare, I'm trying to add a 4-bit PCM audio sound-effect to my game (writing AUDV0 on every scanline). I got something working in plain 6507 assembly, and now I'm trying to use the ARM to pre-calculate the amplitudes so I can simply read them from a data-stream and write to AUDV0 (instead of calculating them on the 6507 each scanline using a 16-bit cycle register and a 16-bit pitch/delta value). However it looks like (partially?) support for that is already in the example project that you shared (referring to methods like 'setNote' and 'setWaveform' in defines_cdsf.h). But I have no idea how to actually set a note from ARM and which predefined(?) datastream to read and store in AUDV0 from assembly code. Also, I was wondering how to keep writing to AUDV0 during the time the ARM is called on VerticalBlank and OverScan. I found this forum post by you, where you say that the 6507 is being fed NOPs during the duration of an ARM subroutine being called. That makes sense, as the 6507 must be doing something while one of the ARM functions is called. And later in that same forum discussion you mention a ZP routine that can run while the ARM is still running, using the 'ldx PosObject' instruction to check if the ARM has finished yet. Do you maybe have a simple example on how to play a single note using 4-bit PCM audio, using CDJF? Maybe you already explained this in one of your earlier posts on DPC+, but I couldn't find it using the forum's search. Cheers, Dion BTW: programming games using CDJF is an amazing experience! I like how it brings me new possibilities, while still I have to fight the limitations of the '2600. Just like my 6502 assembly code, my C code also has to be highly optimized. You can't get sloppy/lazy 🙂
  21. Making headway on Part 7. The menu is fully functional, what's left is: add the routines to colorize X'd options red clean up the code add comments It'll be sometime next week though as this weekend I'm taking my nephew to Fully Charged LIVE in Austin.
  22. the 2600 must output 262 scanlines for those times to be valid. If you look at the 6507 code before the ARM runs: VerticalSync: ldy #2 ldx #VB_TIM64T sty WSYNC sty VSYNC stx TIM64T sty WSYNC sty WSYNC ldy #0 ; 2 2 - zero out some TIA registers while sty GRP0 ; 3 5 we have some free time sty GRP1 ; 3 8 sty WSYNC sty VSYNC ; figure out which ARM Vertical Blank routine to run lda Mode ; $00 = splash, $01 = menu, $80 = game bmi vbgame beq vbsplash ldy #_FN_MENU_VB ; going to run function MenuVerticalBlank() .byte $0c ; NOP ABSOLUTE, skips over ldy #_FN_SPLASH_VB vbsplash: ldy #_FN_SPLASH_VB ; going to run function SplashVerticalBlank() .byte $0c ; NOP ABSOLUTE, skips over ldy #_FN_GAME_VB vbgame: ldy #_FN_GAME_VB ; going to run function GameVerticalBlank() jsr CallArmCode you'll see that both frame 1 and frame 2 will follow the exact same path, thus take the same number of 6507 cycles, resulting in exactly 262 scanlines worth of time transpiring between ARM calls. You can add more 6507 code in there as long as the path taken for frame 1 and frame 2 is identical. Likewise in the C code both frames 1 and 2 will take the same path until they get to the IF/ELSE IF block of code in SplashVerticalBlank(). void SplashVerticalBlank() { int i; int j; int color; int console; color = 0; // default to black // used to show if the console is a 2600 or 7800 console = ((is_7800 ? _SPLASH_78 : _SPLASH_26 ) & 0xfff) + 0x6000; if (frame == 1) // frame is incremented in SplashOverScan() { T1TC = 0; // make sure timer starts at 0 T1TCR = 1; // turn on timer } else if (frame == 2) { T1TCR = 0; // turn off timer after 1 frame // the time it takes to output 262 scanlines is different for // NTSC, PAL, and SECAM consoles, so we can use that to detect // which one and adjust the color values. if (T1TC < (0x11e8ff + 0x11d329)/2) tv_type = NTSC; else if (T1TC > (0x11fd2b + 0x11e8ff)/2) tv_type = PAL; else tv_type = SECAM; } The value in T1TC stops changing as soon as T1TCR is set to 0, so the extra lines of code added for joystick override will have no impact on the values being compared against.
  23. How does that work? are those times specific to that exact code, I mean is it adjusted for any c code or 6507 cycles? Or what exactly do I need to be aware of to not change to make sure It keeps working?
  24. The option was because Draconian was the first time we used it in a released game, and I didn't know if the detection was 100% or not. Would probably be worth doing a poll at some point to see how well it's worked for people. Instead of a menu option, an override could be implemented by checking the joystick state when the 2600's powered up: FIRE for NTSC UP for PAL60 DOWN for SECAM60 To do that I'd change SplashVerticalBlank() to this: void SplashVerticalBlank() { ... else if (frame == 2) { T1TCR = 0; // turn off timer after 1 frame // first check if the user overrode the detection // if they did not then use the value in T1TC to see // how long it took the 2600 to draw 262 scanlines if (JOY0_FIRE) tv_type = NTSC; else if (JOY0_UP) tv_type = PAL; else if (JOY0_DOWN) tv_type = SECAM; else if (T1TC < (0x11e8ff + 0x11d329)/2) tv_type = NTSC; else if (T1TC > (0x11fd2b + 0x11e8ff)/2) tv_type = PAL; else tv_type = SECAM; } ... }
  25. Thanks Darrell, this NTSC/PAL/SECAM detection is really useful! In your games that implement this detection technique (e.g. Draconian) I see that you still offer the option to change the TV type on the menu screen (with the detected TV type pre-selected). Is this because the detection isn't 100%, or just to give people the option to change it if they want to?
  26. Correct, the virtual sprites are not tied to a specific player. Blog entry It's full of stars! goes into some detail on how its done. If you play Draconian in Stella with Fixed Debug Colors mode turned on you can see it in action. In this sequence of frames I drew a white box in an area that contains a mine, the player's ship, and an asteroid. Over 3 frames those objects are drawn like this: player0 - asteroid player1 - player's ship player0 - mine player1 - asteroid player0 - player's ship player1 - mine
  27.  
  • Recently Browsing   0 members

    No registered users viewing this page.


×
×
  • Create New...