Jump to content
IGNORED

IntyBASIC compiler v0.9 recharged! ;)


nanochess

Recommended Posts

Thank you kind sirs! Now I get to decide how much I want to uglify some very pretty BASIC code with a shit-ton of inline ASM. Plus figure out how to make sure I'm not clobbering things with frankencode. Or just wait until the next compiler release :P

 

At least I don't feel quite so silly about asking this. Obviously this is one of those problems that a lot of you have had to code solutions for. And it's not something that would be commonly encountered on the INTV - high score games like Astrosmash were not the norm on this platform.

 

And yup, let's all consider this a feature request of sorts. While the INTV was never the system for twitchy high score games (ie: shooters), I really like the idea of extending it in that direction. Beyond the sheer amount of card data shenanigans, I'm pretty sure one could come up with a pretty good Galaga clone.

Link to comment
Share on other sites

Thank you kind sirs! Now I get to decide how much I want to uglify some very pretty BASIC code with a shit-ton of inline ASM. Plus figure out how to make sure I'm not clobbering things with frankencode. Or just wait until the next compiler release :P

 

 

Well hopefully nanochess is reading the thread. Like I said, all my stuff is out there for him to use if he wants it and he's welcome to incorporate those in IntyBASIC.

 

If he wants to use JLP's multply/divide acceleration, the details are upthread. Use the --jlp flag in jzIntv to enable the features. And I'm always available to answer questions and elaborate.

Edited by intvnut
  • Like 1
Link to comment
Share on other sites

Got any programming specs to share publicly for that? It's kinda hard to develop for a feature like that without an emulator or hardware that supports it, so right now that's something only you can leverage.

An IntyBASIC Bee3/Hive support package that exposes the accelerated functionality is on my list of things to do but I have my hands full at the moment. I'll liaise with nanochess about that, when its time.

Link to comment
Share on other sites

In IntyBasic "Clowns and Balloons", I stored the score as a BCD number, rather than as a binary number.

 

I used 3 bytes to represent the 6 digit score. Normally a byte can contain values from $00 to $ff, but I limit the values to numbers that look like decimal numbers.

As a result, it is trivial to display the number.

 

The tricky part is adding numbers to the score (or doing any other kind of math involving the score, but all I usually do with scores is add numbers to them and compare them to threshold numbers.)

 

To add a number to the score, I add the BCD version of the number, and then I check each digit (starting with the least significant one) to make sure it is less than $a. If not, I add 6 to that digit, which fixes everything up.

 

For example, if the score is $0100 , and want to add 50 points, I add $0050 to it and the result is:

 

$0100 + $0050 -> $0150

 

which looks right, so no fixup is needed.

 

If I add 50 points again, the result is:

 

$0150 + $0050 -> $01a0

 

which no longer looks like a decimal number. To fix it up I add in $0060, and the result is:

 

$01a0 + $0060 -> $0200

 

so it looks like a proper decimal number again.

 

Here's the code (the score is stored in 3 bytes, s1 being the most significant, s2 the middle, s3 the least significant...

REM *****************************************************************
REM
REM add_points - adds point to the score, points must be in bcd format
REM
REM *****************************************************************

add_points: PROCEDURE
	rem temp has the points (must be less than 100)
	temp2 = s3+ temp
	carry= 0
	gosub fix_bcd
	s3=temp2
	if (carry>0) THEN temp2=s2: gosub fix_bcd:s2=temp2
	if (carry>0) THEN temp2=s1: gosub fix_bcd:s1=temp2
	RETURN
	END

fix_bcd: PROCEDURE
	rem value in temp2, read/set carry
	if carry>0 THEN temp2=temp2+1: carry=0
	if ((temp2 AND $0f) > 9) THEN temp2= temp2+6
	if (temp2 > $99) THEN temp2 = (temp2+ $060) AND $00ff: carry=1
	RETURN
	END
REM *****************************************************************
REM
REM display score - draw the score and bounces and lifes
REM
REM *****************************************************************
display_score:	PROCEDURE

	
	rem print score
	PRINT AT (20 * 11) COLOR 2,"S:"

	if (s1=0 ) GOTO ds_trys2
	if (s1> 9) THEN PRINT (s1/16 +16)*8+6
	PRINT ((s1 AND $0f)+16) *8+6
	PRINT (s2/16 +16)*8+6,((s2 AND $0f)+16) *8+6
	PRINT (s3/16 +16)*8+6,((s3 AND $0f)+16) *8+6
	RETURN

ds_trys2:
	if (s2= 0) GOTO ds_trys3
	if (s2> 9) THEN PRINT (s2/16 +16)*8+6
	PRINT ((s2 AND $0f)+16) *8+6
	PRINT (s3/16 +16)*8+6,((s3 AND $0f)+16) *8+6
	RETURN
ds_trys3:
	if (s3> 9) THEN PRINT (s3/16 +16)*8+6
	PRINT ((s3 AND $0f)+16) *8+6


	RETURN
	END
Link to comment
Share on other sites

I guess if you have known numbers to divide by, for instance if you're always dividing by 5, you're better off building your own optimized routine... or just divide by 4 and call it close enough. ;)

 

Well, that's effectively what PRNUM16 does. The first part, not the second part. ;-)

 

PRNUM32 actually works by cracking a 32 bit number into two 5 digit halves, and calling PRNUM16 twice. There's a big divide that's hard coded to divide by 100,000 at the top of that code. PRNUM16 has some extra code to handle printing numbers in the range 65536 - 99999.

Link to comment
Share on other sites

 

In IntyBasic "Clowns and Balloons", I stored the score as a BCD number, rather than as a binary number.

 

 

Ah, good ol' BCD. :-) On the 6502, it'd handle that compensation for you.

 

Would Radix-100 be a good tradeoff point in the middle? You still have to do divmod 10 on each byte to display, but it kinda splits the cost between display and update.

 

(By Radix-100, I mean store two digits in each byte, but don't bother to make each nibble look like a decimal digit. Just store binary values 0 - 99 in there, and ignore the range 100 - 255.)

  • Like 1
Link to comment
Share on other sites

 

Ah, good ol' BCD. :-) On the 6502, it'd handle that compensation for you.

 

Would Radix-100 be a good tradeoff point in the middle? You still have to do divmod 10 on each byte to display, but it kinda splits the cost between display and update.

 

(By Radix-100, I mean store two digits in each byte, but don't bother to make each nibble look like a decimal digit. Just store binary values 0 - 99 in there, and ignore the range 100 - 255.)

Might be a good compromise - it makes the fixup part simplier, but the display takes a little more work...

I'll have to give it some thought...

Link to comment
Share on other sites

catsfolly, this is kinda the path my brain was heading down. Appreciate an example I can work off of. It's how we used to handle this stuff on other platforms in another life, it's just been so bloody long and I've been so spoiled with hardware and compiler help over the years that my brain just hiccuped. I like some of your shortcuts.

 

With all due respect to the good folks that create hardware enhancements, I'm not keen on arithmetic routines that require custom hardware (and subsequent emulator support). Not for the kind of games I'm interested in. I really want vanilla code for everything, that is guaranteed to run on any PCB, in any multicart, and in any existing emulator.

Link to comment
Share on other sites

catsfolly, this is kinda the path my brain was heading down. Appreciate an example I can work off of. It's how we used to handle this stuff on other platforms in another life, it's just been so bloody long and I've been so spoiled with hardware and compiler help over the years that my brain just hiccuped. I like some of your shortcuts.

 

With all due respect to the good folks that create hardware enhancements, I'm not keen on arithmetic routines that require custom hardware (and subsequent emulator support). Not for the kind of games I'm interested in. I really want vanilla code for everything, that is guaranteed to run on any PCB, in any multicart, and in any existing emulator.

 

I believe that the idea of hardware-accelerated features is more useful for games released on cartridge, where the PCB chosen for publication can offer enhanced features that the game can use. For flash-carts, in my opinion, it's only truly worthwhile if it is a de facto standard, or open for others to implement.

 

We shall see what develops in this community. With all this buzz going on now, the future is bright. :)

 

-dZ.

Link to comment
Share on other sites

Might be a good compromise - it makes the fixup part simplier, but the display takes a little more work...

I'll have to give it some thought...

 

If by "more work" you mean cycles, then I'd vote no. At least in terms of how I write things, it's the display that eats the most horsepower - because you're re-drawing the screen constantly. A value like score may not change for quite some time, so if it were up to me, I'd say keep the display as lightweight as possible and let the hard work remain in the variable(s) calculator.

Link to comment
Share on other sites

I really want vanilla code for everything, that is guaranteed to run on any PCB, in any multicart, and in any existing emulator.

Its a nice ideal but unfortunately, not all emulators are created equal. I have what I consider to be vanilla code and it doesn't run correctly in either Bliss or Nostalgia. However, its perfectly fine on real hardware and in jzintv. If you are using IntyBASIC and not writing in assembly language then you are much less likely to run into such bugs during your game's development cycle.

Link to comment
Share on other sites

Its a nice ideal but unfortunately, not all emulators are created equal. I have what I consider to be vanilla code and it doesn't run correctly in either Bliss or Nostalgia. However, its perfectly fine on real hardware and in jzintv. If you are using IntyBASIC and not writing in assembly language then you are much less likely to run into such bugs during your game's development cycle.

 

Sure, and that's why testing across multiple platforms is crucial :) And emulation still sees these edge cases pop up on a regular basis.

 

However, there's a difference between a) uncovering a bug in an emulator (or multicart or PCB) by using some real tricky code, and b) deliberately choosing to write code that targets only one platform. And to me it's a big difference. Now, if it's something ridiculously cool, or impossible/impractical to do any other way - then hells yeah. Just look at what the Coleco guys have done with the Super Game Module as an example. But basic arithmetic operations? If there's an equivalent software solution, I don't see why anyone would intentionally do otherwise. Unless it's for commercial motivations (not implying anything towards you, but I have seen this done in the past).

 

I live thinking of where my code might be in 20 years. Keeping it as compatible as possible, to me, is important. It's why, incidentally, I'm so in love with a few of the tool developers here. You guys supply me with compiled Linux binaries for all of your tools, that run on the first try with zero config. It's heaven to not be tied to one platform. :D

Link to comment
Share on other sites

 

If by "more work" you mean cycles, then I'd vote no. At least in terms of how I write things, it's the display that eats the most horsepower - because you're re-drawing the screen constantly. A value like score may not change for quite some time, so if it were up to me, I'd say keep the display as lightweight as possible and let the hard work remain in the variable(s) calculator.

 

If the score doesn't change, then why are you redrawing it on the screen? :-)

 

In any case, a Radix-100 representation combines well with a "decision tree" approach to display. Basically, you use nested if/else conditions to determine the leading digit. It takes 3-4 branches to decode the first digit, but once you have, you also have the second digit. If I recall correctly, it's faster on average than repeated subtract-by-10 across all leading digits 0..9,

Link to comment
Share on other sites

If the score doesn't change, then why are you redrawing it on the screen? :-)

 

Because the screen gets re-drawn every time something's going on, ie: pretty much constantly. I've thought about how to break out the more static elements and have them only update as necessary, but it seems like a lot of work for little gain. Until now, writing a few digits to the screen hasn't caused me any undue grief. It's only when I need LOTS of digits that it's a problem - and the LOTS comes from continually adding to the score. So... I dunno. Is there a game concept out there where you don't score very often, but the score count is a high number? Other than pinball? :P Actually it's also because I've put score counters and other things right in the main game playfield in some cases. It gets.. weird if you don't re-draw it, depending on what you do.

 

I got a general BASIC coding style question for y'all: I noticed in catsfolly's code that he adds an END after each procedure, even when there's one or more RETURNS in there. Is this just sanity checking in case you get sloppy with your branching? I'm trying to dredge up some 25 year old memories here and am coming up blank. I actually kinda thought that END and RETURN perform the same function in IntyBASIC, but have forgotten all about this after day one of using it. I don't put ENDs anywhere.

Link to comment
Share on other sites

Because the screen gets re-drawn every time something's going on, ie: pretty much constantly. I've thought about how to break out the more static elements and have them only update as necessary, but it seems like a lot of work for little gain. Until now, writing a few digits to the screen hasn't caused me any undue grief. It's only when I need LOTS of digits that it's a problem - and the LOTS comes from continually adding to the score. So... I dunno. Is there a game concept out there where you don't score very often, but the score count is a high number? Other than pinball? :P Actually it's also because I've put score counters and other things right in the main game playfield in some cases. It gets.. weird if you don't re-draw it, depending on what you do.

 

I got a general BASIC coding style question for y'all: I noticed in catsfolly's code that he adds an END after each procedure, even when there's one or more RETURNS in there. Is this just sanity checking in case you get sloppy with your branching? I'm trying to dredge up some 25 year old memories here and am coming up blank. I actually kinda thought that END and RETURN perform the same function in IntyBASIC, but have forgotten all about this after day one of using it. I don't put ENDs anywhere.

It basically has to do with what happens with a GoSub. In really simple terms (not directed at you but for the benefit of others reading) when you use GoSub, a return point is added to the Stack. Exiting a GoSub will pop the value off the stack as part of returning.

 

The END ends the code block and returns to the line after the calling Gosub. A RETURN is used inside of the procedure to exit anytime before the END and return to the caller. If you don't use Return you'd have to jump to a label just before the END. If you GOTO out of procedure instead of using RETURN or falling thru to the END you will leave an entry on the stack, basically tying up memory.

Edited by Tarzilla
Link to comment
Share on other sites

 

 

 

I got a general BASIC coding style question for y'all: I noticed in catsfolly's code that he adds an END after each procedure, even when there's one or more RETURNS in there. Is this just sanity checking in case you get sloppy with your branching? I'm trying to dredge up some 25 year old memories here and am coming up blank. I actually kinda thought that END and RETURN perform the same function in IntyBASIC, but have forgotten all about this after day one of using it. I don't put ENDs anywhere.

In Intellivision assembly language, the code is in blocks called procedures. The procedures aren't nested, once you start one, you can't start another until you end the current procedure with an "end procedure" command (ENDP).

 

In assembly , the "end procedure command" doesn't generate a return. So if the executing code can reach the end of the block, you have to put:

RETURN

ENDP

if you want your code to not continue into the next block of code.

 

So I used the same style of coding in my IntyBasic programs. Maybe this was necessary in early versions of IntyBasic. I can't remember.

 

But now it seems that the END command automatically generates a " RETURN and ENDP " sequence, so there is no reason to have the RETURN and END statements in basic at the end of the procedure.

 

The bottom line - put an END at the last line of the procedure, and use RETURN if you want to stop executing the procedure somewhere in the middle and go back to the calling code.

Edited by catsfolly
Link to comment
Share on other sites

Wow! this discussion is going very fast and there are very good ideas and questions here! :)

 

Let me read this slowly for making my mind about enhancements for IntyBASIC ;)

 

About DIM it can be invoked anywhere, once the compiler reads the statement the space is assigned, it must be before ANY assignment or use of the array.

 

All variables declared/used with IntyBASIC are initialized to zero at start.

 

For big scores I would suggest using a small array DIM score(4) and increment per digit, this saves big cycles in division and multiplication.

 

Joe, thanks for your public domain routines! :thumbsup: I have to recognize my succesive addition/substraction routines currently aren't so fast :D

Link to comment
Share on other sites

Still not following on the END/RETURN stuff, guys. I've always assumed - and in practice so far this seems to bear out - that you could just have every procedure finish up with RETURN and be safe. ie: I don't understand the need to use END anywhere. Sure, you could have multiple (conditional) RETURNs within a procedure, and you want to make damned sure you have the final one. But I don't understand when END will ever be needed.

 

I have reams of code that are basically this:

 

 

GOSUB proc1

GOSUB proc2

GOSUB proc3

 

proc1: PROCEDURE

insert code here

RETURN

 

proc2: PROCEDURE

insert code here

RETURN

 

proc3: PROCEDURE

insert code here

RETURN

 

And it seems to work without issue. I've let this jump in and out of procedures for literally hours without blowing the stack or other weirdness. Not one END anywhere in my code. In fact, I've also nested GOSUBs without problems (ie: within proc2, GOSUB proc3). All of it works for me. Perhaps nanochess needs to chime in on this? Maybe the compiler is saving me from myself? :P

 

 

Tarzilla, your comments about blowing the stack have already caught me, and I think I'm part of the reason nanochess added a stack overflow check command into 0.9 :)

Link to comment
Share on other sites

 

Because the screen gets re-drawn every time something's going on, ie: pretty much constantly. I've thought about how to break out the more static elements and have them only update as necessary, but it seems like a lot of work for little gain. Until now, writing a few digits to the screen hasn't caused me any undue grief. It's only when I need LOTS of digits that it's a problem - and the LOTS comes from continually adding to the score. So... I dunno. Is there a game concept out there where you don't score very often, but the score count is a high number? Other than pinball? :P Actually it's also because I've put score counters and other things right in the main game playfield in some cases. It gets.. weird if you don't re-draw it, depending on what you do.

 

 

I was thinking that if the score didn't update terribly often, you'd just print the new score right where you're adding to the score so it's all in one spot. In that math, if you merely shift cycles from updating the score to displaying the score, it's a wash.

 

But, if you really are calling the "display score" code, say, once per frame, and the score update code is called much less than that (seems like a reasonable assumption), then yeah, you really do want your display update code to be as fast as possible.

Link to comment
Share on other sites

Still not following on the END/RETURN stuff, guys. I've always assumed - and in practice so far this seems to bear out - that you could just have every procedure finish up with RETURN and be safe. ie: I don't understand the need to use END anywhere. Sure, you could have multiple (conditional) RETURNs within a procedure, and you want to make damned sure you have the final one. But I don't understand when END will ever be needed.

 

Even in an unstructured language like Basic, in any of it's flavors, structure is good...

 

Here is a better example if you were trained like me to believe that RETURN and END are different things: RETURN exits the Procedure based on a condition and END defines the END of the code block and will also RETURN if the procedure code doesn't RETURN for some reason in the code above it.

GOSUB proc1
GOSUB proc2
GOSUB proc3

proc1: PROCEDURE
  insert code here
  insert code here to check if x=20: Is it? Then RETURN
  Do some more stuff
  insert code here to check if Y>96: Is it? Then RETURN
  Do some more stuff

END (...of any and all code for proc1 with an Assumed Return)

proc2: PROCEDURE
  insert code here
  insert code here to check if x=20: Is it? Then RETURN
  Do some more stuff
  insert code here to check if Y>96: Is it? Then RETURN
  Do some more stuff

RETURN

X=5
y=10

proc3: PROCEDURE
 insert code here
RETURN

Proc1 is my way of doing things, Proc2 and 3 are without using END. After returning to this code after 5 months after I wrote it, WTF is with the x=5/y=10? Did I forget a Label used by a GOTO (I use almost no GOTOs in my code, they beat GOTOs out of you with a stick in Structured Programming classes) Was I starting another Proc and forgot to finish? Is it orphan code?

 

Since proc1 is doing several things, if you don't use an END, how do you quickly know when the logically related code is part of the same Procedure and not just an editing mistake? Many high level languages like Pascal require similar Define the Procedure /Define the End of Proc structures. Similar to If/EndIF, it makes code review/debugging much easier. I just find it natural to do the following when deciding to use a GOSUB

 

UpdatePlayerLocation: Procedure

END

 

Then start stuffing code inbetween. This way even if I get side tracked (which is frequent) at least I have the logical stub ready.

 

Ultimately INTYBasic will let you ignore END and just use RETURN, my college profs would fail you ;-)

Link to comment
Share on other sites

Still not following on the END/RETURN stuff, guys. I've always assumed - and in practice so far this seems to bear out - that you could just have every procedure finish up with RETURN and be safe. ie: I don't understand the need to use END anywhere. Sure, you could have multiple (conditional) RETURNs within a procedure, and you want to make damned sure you have the final one. But I don't understand when END will ever be needed.

 

 

Ok, I just took a look at the output from IntyBASIC. Both "RETURN" and "END" will generate a return that pops from the stack. Writing both in a subroutine gives you two return instructions in the generated code.

 

If you use RETURN without END, you should get an assembler error, as IntyBASIC opens each PROCEDURE with a PROC directive in the resulting assembly code, and only closes it with ENDP if you use END. So, your BASIC program should fail to assemble with AS1600.

 

(There were some experimental versions of AS1600 that allowed PROCs to nest; these should still complain about PROCs being open at the end of input.)

 

Thus, the upshot is that you should use END at the end of a PROCEDURE. "RETURN; END" is harmless but it generates an extra, unneeded instruction.

 

 

 

But then what college professors teach BASIC?

 

When I was in college, they actually had a CS class that taught TrueBASIC. I think it was mainly for non-CS majors to fulfill their programming language graduation requirement.

Edited by intvnut
Link to comment
Share on other sites

Ok, I experimented some more. My current AS1600 still has nested PROCs enabled, actually, and doesn't seem to complain about open PROCs at the end.

 

You do end up with weird stuff in the epilog, though. Consider these symbols in the symbol table:

000050C1 Q1.Q3.CPYBLK.1             000050C2 Q1.Q3.CPYBLK.2
???????? intybasic_keypad           ???????? _cnt1_p0
???????? Q1.Q3._wait.2              ???????? _cnt1_p1
000050D9 _keypad_table              ???????? Q1.Q3._wait.3
???????? Q1.Q3._wait.4              ???????? _cnt1_key
???????? _cnt2_p0                   ???????? Q1.Q3._wait.5
???????? _cnt2_p1                   ???????? Q1.Q3._wait.6
???????? Q1.Q3._wait.7              ???????? _cnt2_key
00000102 _int                       000050D4 Q1.Q3._wait.1
000050F1 _pal2_vector               00005105 _int_vector
00005100 Q1.Q3._pal2_vector.1       00005133 Q1.Q3._int_vector.vi0 
0000511B Q1.Q3._int_vector.vi14     00000109 _border_color
0000010A _border_mask               00000358 _col0
???????? intybasic_scroll           ???????? _scroll_x
???????? _scroll_y                  00000340 _mobs
00005160 Q1.Q3._int_vector.vi2      ???????? intybasic_music
???????? Q1.Q3._int_vector.vo97     ???????? _music_frame
???????? Q1.Q3._int_vector.vo14     ???????? Q1.Q3._int_vector.vo15
???????? _emit_sound                00000105 _load_gram
0000518F Q1.Q3._int_vector.vi1      0000033D _gram_bitmap
00000106 _gram_target               00000107 _gram_total
0000517C Q1.Q3._int_vector.vi3      ???????? _scroll_d
???????? Q1.Q3._int_vector.vi4      ???????? Q1.Q3._int_vector.vi5
???????? Q1.Q3._int_vector.vi6      ???????? Q1.Q3._int_vector.vi7
???????? Q1.Q3._int_vector.vi8      ???????? Q1.Q3._int_vector.vi12
???????? Q1.Q3._int_vector.vi11     00000324 _scroll_buffer
???????? Q1.Q3._int_vector.vi10     ???????? Q1.Q3._int_vector.vi13
???????? Q1.Q3._int_vector.vi9      00000104 _rand
0000033B _frame                     ???????? Q1.Q3._int_vector.vo98

See those Q1.Q3 labels? That means each function got embedded in the one before it. That looks like a huge bug. I'm surprised it assembled. Probably works because only local labels get affected by the nesting.

 

 

EDIT: Perhaps PROCEDURE should imply an END, so you can't fall from one PROCEDURE to the next? That is, the IntyBASIC compiler could infer an END if one's missing. For the main code, IntyBASIC should probably put a DECR PC or similar to prevent the main code from falling into the first PROCEDURE.

Edited by intvnut
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...