From
Ivan Roshin
→
To
All
26 January 2002
Hello, All!
═══════════════════ start_1 .W ══════════════════
(c) Ivan Roshchin, Moscow
Fido: 2:5020/689.53
ZXNet: 500:95/462.53
E-mail: asder_ffc@softhome.net
WWW: http://www.ivr.da.ru
Optimization using the example of intro "Start"
════════════════════════════════════
("Radiomir. Your computer" 7-10/2001)
Surely you have heard about the largest Russian demoparty 2000
of the year - Chaos Constructions 000. My
works, one of which is the 512-byte intro "Start".
What is intro? This is a small program that combines
graphic effects and (usually) musical accompaniment.
Creating an intro without any exaggeration can be called real
art. Often a program whose running time is
a few minutes, they write for many months. And then, on
demoparty, viewers choose the best intro, which implements
the most beautiful effects for a given size limit.
Competitions (intro compo) are held in various categories:
512 b, 1 Kb, 4 Kb and so on. These numbers determine the maximum
length of the intro code block.
After booting from disk, as a rule, the intro can
use all available RAM. Therefore, the authors do not
they especially try to optimize the intro in length, preferring
write "loose" code and then compress it with specialpacker programs (of which there are more than a dozen).
This is quite justified: in most cases automatic
packaging gives better results compared to manual
optimization, not to mention the investment of time and effort.
But this method is not suitable for 512-byte intros. I think you
already guessed why: an unpacking program, even the most
primitive LZ, will take up a relatively large amount of space compared to
the intro itself. So you have to optimize the code manually,
using many known methods and inventing new ones
and winning byte by byte as a result. It's not enough for this
just know assembler well - you need to have a clear understanding
imagine what each command will look like in machine code and for
how many cycles will it execute; need to know perfectly
ZX Spectrum architecture, including many undocumented
features... In general, intro optimization is not a task for
"dummies". :-)
In this article I just want to look at the example of my
intro "Start" use of various optimization techniques.
Perhaps among them there will be those whose existence you know
didn't even suspect it! Not all techniques are specific to the ZX Spectrum -
something may be useful even if you have absolutely no
another computer.
First, I’ll say a few words about what we will see,
by launching intro. You will need this information to understand -and what exactly does the program do?
Intro can work on any ZX Spectrum 48/128, but
"Pentagon" and faster models are recommended - firstly,
to make pre-calculations faster, and
secondly - so that the screen is cleared at the beginning and at the end
"instantly" (see section "Increasing performance due to
opening cycles").
After startup, about 12 seconds are spent on preliminary
calculations - at this time you can only wait, looking at
black screen. Then the “attribute plasma” effect appears.
(All the beauty of this effect is in movement, and the motionless drawing
alas, I am not able to convey it.)
┌─────────────────────────────┐
│pic_1.scr │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└─────────────────────────────┘
Fig. 1
There is musical accompaniment (by the way, out of six
exhibited at Chaos Constructions 512 byte intro music
was only in "Start"!). Intro duration - 48 seconds,according to the length of the melody. Along with 12 seconds at the beginning
it turns out exactly a minute - this was exactly the limit in
party rules
So, with what the intro looks like for an inexperienced viewer,
we figured it out. If interested, you can download it from my
web pages, the address of which is indicated at the beginning of the article, and all of them themselves
see and hear. And now, as I promised, let’s look at the intro
from the inside. :-)
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
26 January 2002
Hello, All!
═══════════════════ start_1 .W ══════════════════
(c) Ivan Roshchin, Moscow
Fido: 2:5020/689.53
ZXNet: 500:95/462.53
E-mail: asder_ffc@softhome.net
WWW: http://www.ivr.da.ru
Optimization using the example of intro "Start"
════════════════════════════════════
("Radiomir. Your computer" 7-10/2001)
Surely you have heard about the largest Russian demoparty 2000
of the year - Chaos Constructions 000. My
works, one of which is the 512-byte intro "Start".
What is intro? This is a small program that combines
graphic effects and (usually) musical accompaniment.
Creating an intro without any exaggeration can be called real
art. Often a program whose running time is
a few minutes, they write for many months. And then, on
demoparty, viewers choose the best intro, which implements
the most beautiful effects for a given size limit.
Competitions (intro compo) are held in various categories:
512 b, 1 Kb, 4 Kb and so on. These numbers determine the maximum
length of the intro code block.
After booting from disk, as a rule, the intro can
use all available RAM. Therefore, the authors do not
they especially try to optimize the intro in length, preferring
write "loose" code and then compress it with specialpacker programs (of which there are more than a dozen).
This is quite justified: in most cases automatic
packaging gives better results compared to manual
optimization, not to mention the investment of time and effort.
But for 512-byte intros this method is not suitable. I think you
already guessed why: an unpacking program, even the most
primitive LZ, will take up a relatively large amount of space compared to
the intro itself. So you have to optimize the code manually,
using many known methods and inventing new ones
and winning byte by byte as a result. It's not enough for this
just know assembler well - you need to have a clear understanding
imagine what each command will look like in machine code and for
how many cycles will it execute; need to know perfectly
ZX Spectrum architecture, including many undocumented
features... In general, intro optimization is not a task for
"dummies". :-)
In this article I just want to look at the example of my
intro "Start" use of various optimization techniques.
Perhaps among them there will be those whose existence you know
didn't even suspect it! Not all techniques are specific to the ZX Spectrum -
something may be useful even if you have absolutely no
another computer.
First, I’ll say a few words about what we will see,
by launching intro. You will need this information to understand -and what exactly does the program do?
Intro can work on any ZX Spectrum 48/128, but
"Pentagon" and faster models are recommended - firstly,
to make pre-calculations faster, and
secondly - so that the screen is cleared at the beginning and at the end
"instantly" (see section "Increasing performance due to
opening cycles").
After startup, about 12 seconds are spent on preliminary
calculations - at this time you can only wait, looking at
black screen. Then the “attribute plasma” effect appears.
(All the beauty of this effect is in movement, and the motionless drawing
alas, I am not able to convey it.)
┌─────────────────────────────┐
│pic_1.scr │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└─────────────────────────────┘
Fig. 1
There is musical accompaniment (by the way, out of six
exhibited at Chaos Constructions 512 byte intro music
was only in "Start"!). Intro duration - 48 seconds,according to the length of the melody. Along with 12 seconds at the beginning
it turns out exactly a minute - this was exactly the limit in
party rules
So, with what the intro looks like for an inexperienced viewer,
we figured it out. If interested, you can download it from my
web pages, the address of which is indicated at the beginning of the article, and all of them themselves
see and hear. And now, as I promised, let’s look at the intro
from the inside. :-)
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
26 January 2002
Hello, All!
═══════════════════ start_2 .W ══════════════════
Location of data structures
────────────────────────────
First, let's give some definitions and explanations.
_Tables_, _arrays_ are ordinary one-dimensional arrays.
These are the ones I will keep in mind when talking about data structures.
For convenience, I call tables those arrays that do not
change during program operation.
Unless otherwise noted, the length of each element
assumed to be one byte.
_Segment_ - a memory section 256 bytes long, starting
from an address that is a multiple of 256. Let me remind you that 256 is 100 in
hexadecimal number system. Therefore, junior
the segment start address byte is zero. Segment definitely
determined by the high byte of its start address. High bytes
the addresses of all cells in the segment are the same.
Let's call two segments _adjacent_ if one of them
directly follows the other. Property of adjacent segments -
if you take any memory cell from one segment and any of
another, then the high bytes of their addresses will differ by
unit.
Let's call the data structure _aligned to the segment boundary_,
if it starts from the beginning of this segment. Obviously junior
the address byte of the first element of such a structure is zero.
Let's call a data structure _single-segment_ if it
lies entirely in any one segment. All elementsIn this structure, the high byte of the address is the same.
Table TABLE_SIN (line 14), table PALETTE (line 18)
and the array with the contents of the AY registers (lines 60-61) are aligned to
segment boundary. What is the reason for this arrangement of these
data structures?
Firstly, it simplifies access to their elements. Valid
but to access any element, you must first
calculate its address. If each element takes up one byte (and
for the above data structures this is exactly the case), then for
to calculate the address of the i-th element, you need to add i to the address
the beginning of the structure. So, if the structure is aligned to the boundary
segment (and then, as we remember, the low byte of the address of its beginning
is equal to zero), and the number of elements of this structure does not exceed 256
(which means it is single-segment), then the high byte of the address of the i-th
element will be equal to the most significant byte of the address of the beginning of the structure, and
the low byte will be equal to i. Thus, when calculating the address
you can do without the addition operation and still win in
speed and program length.
Secondly, the addresses of all elements of such a data structure
differ only in the low byte. Therefore, when
sequential access to elements, it is enough to increase
(decrement) only the low byte of the address, which also provides
gain in speed and program length.
Thirdly, if the length of the structure is 256 bytes, thenthe possibility of its automatic looping, that is, transition
from last element to first and from first to last without
any special checks. When we increase junior
byte address of the last element of the structure, then automatically
we get the address of its first element (for example, HL=#80FF, with the command
INC L increase the low byte of the address and get HL=#8000).
Similarly, when we decrement the low byte of the address of the first
element of the structure, then we automatically obtain its address
last element (HL=#8000, DEC L -> HL=#80FF). This is very
convenient if the data structure contains periodic values
functions (TABLE_SIN) or values that can be interpreted
in a similar way (smooth color transitions in the PALETTE table).
Fourth, this arrangement of the data structure
provides the ability to optimize the processing of all its elements
in a loop (for more details, see "Optimizing Loops").
As you can see, the TABLE_SIN table location address is
equals #AA00. Why was this particular address chosen and not
any other multiple of 256? The point is that by the time
the address TABLE_SIN must be placed in the register pair DE, in
register D already contains the number #AA. It is also known that
The accumulator value by this time is zero. And single-byte
with the command LD E,A (line 147) we place the address we need in DE.
And if the TABLE_SIN address were different, we would have to usethree-byte command LD DE,TABLE_SIN.
The PALETTE table and the TABLE_SIN table are located in adjacent
segments. This was done for a reason. In the inner loop
effect "plasma" (lines 238-264) work is in progress with one
table, then on the other. The H register contains the highest
byte of the address of the currently used table. Since these
high bytes differ by one, instead of rebooting
register H using the LD H,n instruction becomes possible
using the INC H and DEC H commands (lines 252, 260), thanks
which gives us a gain in both length and speed.
Note the data structure containing the information
about musical patterns (lines 753-810). This is a single segment
structure. What does this give? Ease of reference to its elements. Everyone
an element is uniquely specified by the low byte of its address, so
as the most significant byte of the address is the same for all elements.
Thanks to this, in the POSITIONS table (lines 402-430), where
the order of playing patterns is determined, each element
(which is a link to information about the corresponding pattern)
takes up one byte. And if the link indicated the full address,
it would take up two bytes.
Selecting the most advantageous data presentation
────────────────────── ───────────────────────
The music for the intro was written in the Pro Tracker 3.4 editor.
The length of the module compiled with the player turned out to be equal to4558 bytes. But, according to the conditions, the length of the intro should not be
exceed 512 bytes! How was this contradiction resolved?
Significant reduction in size (and in addition - time)
execution of the playback procedure!) was achieved due to
representation of the data necessary to play music in
the most advantageous form. The choice of this form was largely
due to the individual characteristics of the melody. Let's consider this
with examples.
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
26 January 2002
Hello, All!
═══════════════════ start_2 .W ══════════════════
Location of data structures
────────────────────────────
First, let's give some definitions and explanations.
_Tables_, _arrays_ are ordinary one-dimensional arrays.
These are the ones I will keep in mind when talking about data structures.
For convenience, I call tables those arrays that do not
change during program operation.
Unless otherwise noted, the length of each element
assumed to be one byte.
_Segment_ - a memory section 256 bytes long, starting
from an address that is a multiple of 256. Let me remind you that 256 is 100 in
hexadecimal number system. Therefore, junior
the segment start address byte is zero. Segment definitely
determined by the high byte of its start address. High bytes
the addresses of all cells in the segment are the same.
Let's call two segments _adjacent_ if one of them
directly follows the other. Property of adjacent segments -
if you take any memory cell from one segment and any of
another, then the high bytes of their addresses will differ by
unit.
Let's call the data structure _aligned to the segment boundary_,
if it starts from the beginning of this segment. Obviously junior
the address byte of the first element of such a structure is zero.
Let's call a data structure _single-segment_ if it
lies entirely in any one segment. All elementsIn this structure, the high byte of the address is the same.
Table TABLE_SIN (line 14), table PALETTE (line 18)
and the array with the contents of the AY registers (lines 60-61) are aligned to
segment boundary. What is the reason for this arrangement of these
data structures?
Firstly, it simplifies access to their elements. Valid
but to access any element, you must first
calculate its address. If each element takes up one byte (and
for the above data structures this is exactly the case), then for
to calculate the address of the i-th element, you need to add i to the address
the beginning of the structure. So, if the structure is aligned to the boundary
segment (and then, as we remember, the low byte of the address of its beginning
is equal to zero), and the number of elements of this structure does not exceed 256
(which means it is single-segment), then the high byte of the address of the i-th
element will be equal to the most significant byte of the address of the beginning of the structure, and
the low byte will be equal to i. Thus, when calculating the address
you can do without the addition operation and still win in
speed and program length.
Secondly, the addresses of all elements of such a data structure
differ only in the low byte. Therefore, when
sequential access to elements, it is enough to increase
(decrement) only the low byte of the address, which also provides
gain in speed and program length.
Thirdly, if the length of the structure is 256 bytes, thenthe possibility of its automatic looping, that is, transition
from last element to first and from first to last without
any special checks. When we increase junior
byte address of the last element of the structure, then automatically
we get the address of its first element (for example, HL=#80FF, with the command
INC L increase the low byte of the address and get HL=#8000).
Similarly, when we decrement the low byte of the address of the first
element of the structure, then we automatically obtain its address
last element (HL=#8000, DEC L -> HL=#80FF). This is very
convenient if the data structure contains periodic values
functions (TABLE_SIN) or values that can be interpreted
in a similar way (smooth color transitions in the PALETTE table).
Fourth, this arrangement of the data structure
provides the ability to optimize the processing of all its elements
in a loop (for more details, see "Optimizing Loops").
As you can see, the TABLE_SIN table location address is
equals #AA00. Why was this particular address chosen and not
any other multiple of 256? The point is that by the time
the address TABLE_SIN must be placed in the register pair DE, in
register D already contains the number #AA. It is also known that
The accumulator value by this time is zero. And single-byte
with the command LD E,A (line 147) we place the address we need in DE.
And if the TABLE_SIN address were different, we would have to usethree-byte command LD DE,TABLE_SIN.
The PALETTE table and the TABLE_SIN table are located in adjacent
segments. This was done for a reason. In the inner loop
effect "plasma" (lines 238-264) work is in progress with one
table, then on the other. The H register contains the highest
byte of the address of the currently used table. Since these
high bytes differ by one, instead of rebooting
register H using the LD H,n instruction becomes possible
using the INC H and DEC H commands (lines 252, 260), thanks
which gives us a gain in both length and speed.
Note the data structure containing the information
about musical patterns (lines 753-810). This is a single segment
structure. What does this give? Ease of reference to its elements. Everyone
an element is uniquely specified by the low byte of its address, so
as the most significant byte of the address is the same for all elements.
Thanks to this, in the POSITIONS table (lines 402-430), where
the order of playing patterns is determined, each element
(which is a link to information about the corresponding pattern)
takes up one byte. And if the link indicated the full address,
it would take up two bytes.
Selecting the most advantageous data presentation
────────────────────── ───────────────────────
The music for the intro was written in the Pro Tracker 3.4 editor.
The length of the module compiled with the player turned out to be equal to4558 bytes. But, according to the conditions, the length of the intro should not be
exceed 512 bytes! How was this contradiction resolved?
Significant reduction in size (and in addition - time)
execution of the playback procedure!) was achieved due to
representation of the data necessary to play music in
the most advantageous form. The choice of this form was largely
due to the individual characteristics of the melody. Let's consider this
with examples.
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
26 January 2002
Hello, All!
═══════════════════ start_3 .W ══════════════════
Let's start with the fact that to play music you need a frequency
table with division coefficients for each note. B
universal player this table contains division coefficients
(numbers in the range 0-4095) for all 96 notes. Accordingly,
Such a table takes up 96*2=192 bytes. But, knowing what notes
are involved in the melody, you can store only them in the table
coefficients. The melody written for the intro features
only ten different notes - which means that for storing a table
only twenty bytes are required.
Now let’s pay attention to the range in which they are
coefficient values for these ten notes (lines 360-369). This
range #6E..#114. If the coefficients were in the range
#00-#FF, then to store each of them it would be enough just
one byte, not two. So, it turns out that the coefficients
can be converted to the specified range if first
reduce their values by a certain amount (freq_shift - see
line 371). As a result of this optimization, the frequency table
(FREQ_TABLE) takes half as much - ten bytes (see lines
742-751). And when during the course of the program you need to place in
registers AY value of the division coefficient taken from the table
freq_shift is added to the number (line 701).
By the way, if the division coefficients were even, we couldit would be even simpler: store their values in a table,
divided by two. Then line 701 would contain the command ADD A,A -
one byte shorter than ADD A,freq_shift, and faster to execute.
As you must have already understood from the above, each of
ten notes used in the melody are indicated by a certain number
(in number 0..9 - see lines 349-358), and when you need to find out
the division coefficient corresponding to a given note, this number
serves as an index in the frequency table. It is obvious that in principle
it makes absolutely no difference how it will be established
correspondence between notes and their numbers - as long as there is one
one-to-one. The lowest note does not have to be
assigned number 0, and the highest - 9. Thus, there is
some freedom of choice. I took advantage of this freedom
for further optimization purposes.
To understand the essence of the matter, I’ll tell you a little more about the used
in the intro melody. The melody itself sounds in channels A and B, and in
channel C sounds the accompaniment chords (more precisely, the chord is
when the notes sound simultaneously, and in this case the notes sound at the same time
queues, but quickly replace each other). Total used
four different chords: C5-E5-G5, D5-F5-A5, E5-G5-B5 and A4-C5-
E5. Each of them has its own number. Naturally, there is a need for
a special table, from which, by chord number, one could
extract information about its notes.So, I chose the note numbers in such a way that the frequency
the table also served as a chord chart! Look at
lines 389-392 - there each chord is assigned
some number that is an offset in the ORNAMENTS table
(which in turn is part of the FREQ_TABLE table).
Three consecutive bytes in this table are
division factors for three notes of a chord. Amount of winnings
obtained by combining tables, amounted in this case
eight bytes. For another program, maybe this is not much, but
for a 512-byte intro this is more than one and a half percent!
Let's move on to the sample used in the melody. Here
how it looks in Pro Tracker:
┌─────────────────────────────┐
│pic_2.scr │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└─────────────────────────────┘
Fig. 2
For each element of this sample you need to store a value
amplitude (number #0-#F) and frequency offset (-2, 0 or +2).The data storage format used in intro allows you to fit
both in one byte: in the lower four bits - the value
amplitude, and in the higher two - the frequency offset code (see lines
816-822). Thanks to this, it is possible to reduce both the size of the table
SAMPLE, and the length of those program fragments where
manipulations with this table.
Now pay attention to the table of positions (POSITIONS,
lines 402-430), which indicates the playback order
patterns.
Usually when writing a player they do this: in each element
This table stores the pattern number (one byte), and with the help of
of one table, the pattern number determines its address (length
patterns are not the same). Knowing the number of positions in the melody (24)
and the number of patterns (13), you can calculate the memory consumption when
using this method: 24+13*2=50 bytes.
You can store in each element of the position table
directly the address of the corresponding pattern. In this case
Memory consumption will be 2*24=48 bytes.
However, note that all information about patterns (lines
758-810) lies in the segment #8100-#81FF. This means the high bytes
Pattern addresses will be the same and equal to #81. So why
then store them in a table? Let's leave only the low bytes there, and
the entire table will take only 24 bytes! This is what was done in
intro. How is the pattern address determined?you can look at lines 487-488.
Yes, about patterns: let’s look at how their features influenced
structures to the format in which they are stored.
Let's list these features first. Firstly, in every
eight note pattern. Secondly, each pattern uses
just one chord. Third, the volume in the pattern can either
decrease (from #F to #C), or increase (from #C to #F), or
remain constant (#F). Fourth, many patterns consist
of two alternating notes (for example, like this: C5-G4-C5-G4-C5-G4-C5-
G4).
When choosing the pattern storage format, all this was taken into account, and
as a result, information about one pattern takes up either two, or
five bytes (see lines 753-810). So little? Yes! And that's how it is
it worked.
The first byte of the pattern contains in the most significant two bits
information about the nature of the volume change - one of three
possible values (see lines 378-383). The fifth bit is located
"packing" sign - if this bit is set, then this pattern
consists of two alternating notes (see lines 385-387). Juniors
four bits is the number of the chord used in the pattern (and
more precisely - the offset in the ORNAMENTS table).
In one (for a packed pattern) or four (for
unpacked) the next bytes contain information about
notes - two notes in each byte. I already mentioned above that in
the melody uses only ten different notes - thanks tohalf a byte is enough to store the note number.
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
26 January 2002
Hello, All!
═══════════════════ start_3 .W ══════════════════
Let's start with the fact that to play music you need a frequency
table with division coefficients for each note. B
universal player this table contains division coefficients
(numbers in the range 0-4095) for all 96 notes. Accordingly,
Such a table takes up 96*2=192 bytes. But knowing what notes
are involved in the melody, you can store only them in the table
coefficients. The melody written for the intro features
only ten different notes - which means that for storing a table
only twenty bytes are required.
Now let’s pay attention to the range in which they are
coefficient values for these ten notes (lines 360-369). This
range #6E..#114. If the coefficients were in the range
#00-#FF, then to store each of them it would be enough just
one byte, not two. So, it turns out that the coefficients
can be converted to the specified range if first
reduce their values by a certain amount (freq_shift - see
line 371). As a result of this optimization, the frequency table
(FREQ_TABLE) takes half as much - ten bytes (see lines
742-751). And when during the course of the program you need to place in
registers AY value of the division coefficient taken from the table
freq_shift is added to the number (line 701).
By the way, if the division coefficients were even, we couldit would be even simpler: store their values in a table,
divided by two. Then line 701 would contain the command ADD A,A -
one byte shorter than ADD A,freq_shift, and faster to execute.
As you must have already understood from the above, each of
ten notes used in the melody are indicated by a certain number
(in number 0..9 - see lines 349-358), and when you need to find out
the division coefficient corresponding to a given note, this number
serves as an index in the frequency table. It is obvious that in principle
it makes absolutely no difference how it will be established
correspondence between notes and their numbers - as long as there is one
one-to-one. The lowest note doesn't have to be
assigned number 0, and the highest - 9. Thus, there is
some freedom of choice. I took advantage of this freedom
for further optimization purposes.
To understand the essence of the matter, I’ll tell you a little more about the used
in the intro melody. The melody itself sounds in channels A and B, and in
channel C sounds the accompaniment chords (more precisely, the chord is
when the notes sound simultaneously, and in this case the notes sound at the same time
queues, but quickly replace each other). Total used
four different chords: C5-E5-G5, D5-F5-A5, E5-G5-B5 and A4-C5-
E5. Each of them has its own number. Naturally, there is a need for
a special table, from which, by chord number, one could
extract information about its notes.So, I chose the note numbers in such a way that the frequency
the table also served as a chord chart! Look at
lines 389-392 - there each chord is assigned
some number that is an offset in the ORNAMENTS table
(which in turn is part of the FREQ_TABLE table).
Three consecutive bytes in this table are
division factors for three notes of a chord. Amount of winnings
obtained by combining tables, amounted in this case
eight bytes. For another program, maybe this is not much, but
for a 512-byte intro this is more than one and a half percent!
Let's move on to the sample used in the melody. Here
how it looks in Pro Tracker:
┌─────────────────────────────┐
│pic_2.scr │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└─────────────────────────────┘
Fig. 2
For each element of this sample you need to store a value
amplitude (number #0-#F) and frequency offset (-2, 0 or +2).The data storage format used in intro allows you to fit
both in one byte: in the lower four bits - the value
amplitude, and in the higher two - the frequency offset code (see lines
816-822). Thanks to this, it is possible to reduce both the size of the table
SAMPLE, and the length of those program fragments where
manipulations with this table.
Now pay attention to the table of positions (POSITIONS,
lines 402-430), which indicates the playback order
patterns.
Usually when writing a player they do this: in each element
This table stores the pattern number (one byte), and with the help of
of one table, the pattern number determines its address (length
patterns are not the same). Knowing the number of positions in the melody (24)
and the number of patterns (13), you can calculate the memory consumption when
using this method: 24+13*2=50 bytes.
You can store in each element of the position table
directly the address of the corresponding pattern. In this case
Memory consumption will be 2*24=48 bytes.
However, note that all information about patterns (lines
758-810) lies in the segment #8100-#81FF. This means the high bytes
Pattern addresses will be the same and equal to #81. So why
then store them in a table? Let's leave only the low bytes there, and
the entire table will take only 24 bytes! This is what was done in
intro. How is the pattern address determined?you can look at lines 487-488.
Yes, about patterns: let’s look at how their features influenced
structures to the format in which they are stored.
Let's list these features first. Firstly, in every
eight note pattern. Secondly, each pattern uses
just one chord. Third, the volume in the pattern can either
decrease (from #F to #C), or increase (from #C to #F), or
remain constant (#F). Fourth, many patterns consist
of two alternating notes (for example, like this: C5-G4-C5-G4-C5-G4-C5-
G4).
When choosing the pattern storage format, all this was taken into account, and
as a result, information about one pattern takes up either two, or
five bytes (see lines 753-810). So little? Yes! And that's how it is
it worked.
The first byte of the pattern contains in the most significant two bits
information about the nature of the volume change - one of three
possible values (see lines 378-383). The fifth bit is located
"packing" sign - if this bit is set, then this pattern
consists of two alternating notes (see lines 385-387). Juniors
four bits is the number of the chord used in the pattern (and
more precisely - the offset in the ORNAMENTS table).
In one (for a packed pattern) or four (for
unpacked) the next bytes contain information about
notes - two notes in each byte. I already mentioned above that in
the melody uses only ten different notes - thanks tohalf a byte is enough to store the note number.
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
26 January 2002
Hello, All!
═══════════════════ start_4 .W ══════════════════
So, with the help of a well-chosen data representation, you can
significantly reduce program size and/or increase it
performance. Consider only what is most compact
presentation of data is not always the most beneficial. Don't forget
monitor the length of program fragments processing these
data, and check whether there will be a win in the end.
Using program sections as command codes,
and as data
────────────────────────────── ───────────────────────────────
Pay attention to the memory area at addresses #8001-#8009.
There is a fragment of the sample table construction procedure.
This table is built once at the very beginning
program operation, and then the specified area is used
for storing the contents of the music coprocessor registers
(AY).
When playing a melody in register R7 (byte at address
#8007) should contain the value %??111000 (high two bits
can be any), and in register R10 (byte at address #800A) -
%???01001 (the most significant three bits can be anything). So, in
the program at address #8007 contains the command LD A,B (line 92),
and the code of this command - %01111000 - subsequently serves as
as the value written to the 7th register AY. Likewise,
The XOR C command located at address #800A (line 101) has the code%10101001 and henceforth serves as the value,
written to the 10th register AY. So first the bytes in
cells #8007 and #800A are used as command codes, and then -
as data.
This technique allows you to reduce the length of the program, in this case
case - by ten bytes (there is no need for commands
LD A,%00111000: LD (#8007),A: LD A,%00001001: LD (#800A),A).
Of course, the command codes match the required values
AY registers are not accidental. I had to carefully think through the logic
work of the sample table construction procedure and select the address
compiling the intro so that the necessary commands end up in the right ones
places. It helped that it was absolutely accurate
matching command codes and AY register values was not required -
differences were allowed in the most significant bits that did not affect
sound formation.
Now pay attention to the execution once in the very
at the beginning of the program, the procedure for filling the screen with texture
(lines 121-140). Line 134 contains the command JR Z,GRID_2
(in other words, JR Z,$+3), which in machine code looks like
#28, #01. So, the variable then used in the program
QUARK is located at the same address as the second byte of this
teams. As we can see, value #01 is first used as part of
jump commands and then as the initial value
variable, that is, as data.The QUARK variable could be placed in any free
memory cell - but then before using it for the first time you need to
would be to initialize with the value #01, and this would take more
five bytes (LD A,1: LD (QUARK),A). These five bytes are me and
saved.
Naturally, the procedure for filling the screen with texture
was specially written so that it contained the command JR Z,$+3.
In the section "Selecting the most advantageous data representation"
it was mentioned that the frequency table stores values
division coefficients reduced by the constant freq_shift.
The value of this constant must be such that the reduced
division coefficients were in the range 0..#FF. Based on this,
it is not difficult to come to the conclusion that the value of freq_shift should lie
in the range #15..#6E, i.e. there is a certain freedom of choice.
Taking advantage of this freedom, I chose freq_shift like so,
so that the first element of the FREQ_TABLE table is equal to #C9 (see.
line 371). What is this for? The point is that directly
before the FREQ_TABLE table there is a procedure IZM_FRQ (rows
682-715), ending with the RET command. The code for this command is just
and there is #C9. This means that the RET command can be removed from the program (here
why is it commented out in line 715), and in its quality there will be
act as the first element of the FREQ_TABLE table.
Optimization of placing constants in registers and memory cells──────────────────────────── ─────────────────────────────
Loading constants into registers or writing them to memory
is performed in a real program quite often, and optimization
These actions can bring very tangible results. Oh
some methods of such optimization used in
writing the intro, I’ll tell you.
When it is necessary to reset the accumulator, the command LD A,0 is usually
are replaced by XOR A (see lines 234, 311, 525, 647), which allows
save 1 byte/3 clock cycles on each such replacement. True, this
the command, unlike LD, affects the flags, so you have to be here
attentive. For example, this fragment (lines 522-526):
BIT 5,(HL)
LD A,#23
JR Z,SET_COD
XOR A
SET_COD LD (ADR_CODE),A
Under no circumstances should you write it like this:
BIT 5,(HL)
XOR A
JR NZ,SET_COD
LD A,#23
SET_COD LD (ADR_CODE),A
because after the XOR A command the Z flag will always be set
(it is set by the result of the operation, and the result is
null!), so further checking its value (JR NZ)
loses all meaning and the program will not work correctly.
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
26 January 2002
Hello, All!
═══════════════════ start_4 .W ══════════════════
So, with the help of a well-chosen data representation, you can
significantly reduce program size and/or increase it
performance. Consider only what is most compact
presentation of data is not always the most beneficial. Don't forget
monitor the length of program fragments processing these
data, and check whether there will be a win in the end.
Using program sections as command codes,
and as data
────────────────────────────── ───────────────────────────────
Pay attention to the memory area at addresses #8001-#8009.
There is a fragment of the sample table construction procedure.
This table is built once at the very beginning
program operation, and then the specified area is used
for storing the contents of the music coprocessor registers
(AY).
When playing a melody in register R7 (byte at address
#8007) should contain the value %??111000 (high two bits
can be any), and in register R10 (byte at address #800A) -
%???01001 (the most significant three bits can be anything). So, in
the program at address #8007 contains the command LD A,B (line 92),
and the code of this command - %01111000 - subsequently serves as
as the value written to the 7th register AY. Likewise,
The XOR C command located at address #800A (line 101) has the code%10101001 and henceforth serves as the value,
written to the 10th register AY. So first the bytes in
cells #8007 and #800A are used as command codes, and then -
as data.
This technique allows you to reduce the length of the program, in this case
case - by ten bytes (there is no need for commands
LD A,%00111000: LD (#8007),A: LD A,%00001001: LD (#800A),A).
Of course, the command codes match the required values
AY registers are not accidental. I had to carefully think through the logic
work of the sample table construction procedure and select the address
compiling the intro so that the necessary commands end up in the right ones
places. It helped that it was absolutely accurate
matching command codes and AY register values was not required -
differences were allowed in the most significant bits that did not affect
sound formation.
Now pay attention to the execution once in the very
at the beginning of the program, the procedure for filling the screen with texture
(lines 121-140). Line 134 contains the command JR Z,GRID_2
(in other words, JR Z,$+3), which in machine code looks like
#28, #01. So, the variable then used in the program
QUARK is located at the same address as the second byte of this
teams. As we can see, value #01 is first used as part of
jump commands and then as the initial value
variable, that is, as data.The QUARK variable could be placed in any free
memory cell - but then before using it for the first time you need to
would be to initialize with the value #01, and this would take more
five bytes (LD A,1: LD (QUARK),A). These five bytes are me and
saved.
Naturally, the procedure for filling the screen with texture
was specially written so that it contained the command JR Z,$+3.
In the section "Selecting the most advantageous data representation"
it was mentioned that the frequency table stores values
division coefficients reduced by the constant freq_shift.
The value of this constant must be such that the reduced
division coefficients were in the range 0..#FF. Based on this,
it is not difficult to come to the conclusion that the value of freq_shift should lie
in the range #15..#6E, i.e. there is a certain freedom of choice.
Taking advantage of this freedom, I chose freq_shift like so,
so that the first element of the FREQ_TABLE table is equal to #C9 (see.
line 371). What is this for? The point is that directly
before the FREQ_TABLE table there is a procedure IZM_FRQ (rows
682-715), ending with the RET command. The code for this command is just
and there is #C9. This means that the RET command can be removed from the program (here
why is it commented out in line 715), and in its quality there will be
act as the first element of the FREQ_TABLE table.
Optimization of placing constants in registers and memory cells──────────────────────────── ─────────────────────────────
Loading constants into registers or writing them to memory
is performed in a real program quite often, and optimization
These actions can bring very tangible results. Oh
some methods of such optimization used in
writing the intro, I’ll tell you.
When it is necessary to reset the accumulator, the command LD A,0 is usually
are replaced by XOR A (see lines 234, 311, 525, 647), which allows
save 1 byte/3 clock cycles on each such replacement. True, this
the command, unlike LD, affects the flags, so you have to be here
attentive. For example, this fragment (lines 522-526):
BIT 5,(HL)
LD A,#23
JR Z,SET_COD
XOR A
SET_COD LD (ADR_CODE),A
Under no circumstances should you write it like this:
BIT 5,(HL)
XOR A
JR NZ,SET_COD
LD A,#23
SET_COD LD (ADR_CODE),A
because after the XOR A command the Z flag will always be set
(it is set by the result of the operation, and the result is
null!), so further checking its value (JR NZ)
loses all meaning and the program will not work correctly.
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
26 January 2002
Hello, All!
═══════════════════ start_5 .W ══════════════════
On line 116 you need to place a 0 at the address given in
register pair HL. It would be possible to do this with the LD (HL) command,
0. But it is known that by this moment register B has been reset to zero after
DJNZ. Thus, it is sufficient to use the command LD (HL),B.
The savings are 1 byte/3 clock cycles.
Before filling the screen with the texture (lines 121-140)
it is necessary to place in HL the address of the first byte to be processed
video memory - #57FF. But by this time the HL already contains this
meaning: it ended up there after the CLS procedure performed
cleaning the screen. Therefore, the LD HL,#57FF command is not needed.
The savings are 3 bytes/10 clock cycles.
In line 147, you need to load the value #AA00 into DE (address
tables TABLE_SIN). But we know that D=#AA and A=0 (see lines
142-143). Then instead of the command LD DE,#AA00 you can use
command LD E,A. The savings are 2 bytes/6 clock cycles. Address
table TABLE_SIN was deliberately chosen equal to #AA00 (see section
"Location of data structures") so that this optimization becomes
possible.
In line 189 you need to load the table address into DE
PALETTE. It is known that before this the address was set in DE
tables TABLE_SIN, and these 256-byte tables are located next to each other
behind a friend. Then it is enough to use the INC D command instead
LD DE,PALETTE. The savings are 2 bytes/6 clock cycles.The fact that the tables PALETTE and TABLE_SIN are located in
adjacent segments, is used in the future. Yes, in the line
252 command LD H,PALETTE/256 can be replaced with INC H, since
it is known that register H contains the high byte of the table address
TABLE_SIN. Similarly, on line 260, instead of the LD H command,
TABLE_SIN/256 the DEC H command is used. In both
In this case, the savings are 1 byte/3 clock cycles.
The location in adjacent
segments of the POSITIONS table and pattern data (rows
478-489).
And the fact that the A_AMP variable and the FREQ_TABLE table are located
in one segment, allows you to do without loading at all
high byte of the address (lines 566-582).
In lines 291-294 you need to increase SP by 4 (otherwise
speaking, remove two values stored there from the stack, now
unnecessary) and place 0 at address TRIGGER. It's possible to do this
like this: POP AF: POP AF: XOR A: LD (TRIGGER),A. But it is known that
The most significant byte of the value at the top of the stack is zero
(line 469). Then the XOR A command is not needed - just write
at address TRIGGER the value that will end up in the accumulator
after the first POP AF command. The savings are 1 byte/4 clock cycles.
In lines 475-476, in order to reset DE, we use
two commands LD D,A: LD E,A, despite the fact that A=0. This allows
save 1 byte/2 clock cycles compared to using the LD instruction
DE,0.If you want to learn more about download optimization
constants into registers, I recommend studying [1].
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
26 January 2002
Hello, All!
═══════════════════ start_5 .W ══════════════════
On line 116 you need to place a 0 at the address given in
register pair HL. It would be possible to do this with the LD (HL) command,
0. But it is known that by this moment register B has been reset after
DJNZ. Thus, it is sufficient to use the command LD (HL),B.
The savings are 1 byte/3 clock cycles.
Before filling the screen with the texture (lines 121-140)
it is necessary to place in HL the address of the first byte to be processed
video memory - #57FF. But by this point the HL already contains this
meaning: it ended up there after the CLS procedure performed
cleaning the screen. Therefore, the LD HL,#57FF command is not needed.
The savings are 3 bytes/10 clock cycles.
In line 147, you need to load the value #AA00 into DE (address
tables TABLE_SIN). But we know that D=#AA and A=0 (see lines
142-143). Then instead of the command LD DE,#AA00 you can use
command LD E,A. The savings are 2 bytes/6 clock cycles. Address
table TABLE_SIN was deliberately chosen equal to #AA00 (see section
"Location of data structures") so that this optimization becomes
possible.
In line 189 you need to load the table address into DE
PALETTE. It is known that before this the address was set in DE
tables TABLE_SIN, and these 256-byte tables are located next to each other
behind a friend. Then it is enough to use the INC D command instead
LD DE,PALETTE. The savings are 2 bytes/6 clock cycles.The fact that the tables PALETTE and TABLE_SIN are located in
adjacent segments, is used in the future. Yes, in the line
252 command LD H,PALETTE/256 can be replaced with INC H, since
it is known that register H contains the high byte of the table address
TABLE_SIN. Similarly, on line 260, instead of the LD H command,
TABLE_SIN/256 the DEC H command is used. In both
In this case, the savings are 1 byte/3 clock cycles.
The location in adjacent
segments of the POSITIONS table and pattern data (rows
478-489).
And the fact that the A_AMP variable and the FREQ_TABLE table are located
in one segment, allows you to do without loading at all
high byte of the address (lines 566-582).
In lines 291-294 you need to increase SP by 4 (otherwise
speaking, remove two values stored there from the stack, now
unnecessary) and place 0 at address TRIGGER. It's possible to do this
like this: POP AF: POP AF: XOR A: LD (TRIGGER),A. But it is known that
The most significant byte of the value at the top of the stack is zero
(line 469). Then the XOR A command is not needed - just write
at address TRIGGER the value that will end up in the accumulator
after the first POP AF command. The savings are 1 byte/4 clock cycles.
In lines 475-476, in order to reset DE, we use
two commands LD D,A: LD E,A, despite the fact that A=0. This allows
save 1 byte/2 clock cycles compared to using the LD instruction
DE,0.If you want to learn more about download optimization
constants into registers, I recommend studying [1].
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
26 January 2002
Hello, All!
═══════════════════ start_6 .W ══════════════════
Cycle optimization
──────────────────
You can often benefit from replacing the cycle with advance
a known number of repetitions ("for") per loop with output at
condition ("repeat-until").
Consider, for example, filling the screen with a texture (lines
129-140). The length of the filled area is known in advance - #1800
byte. If we used a "for" loop, we would have to create
a separate counter (and allocate a register pair for it),
enter the number of repetitions into it (#1800), and then in each
pass through the loop, decrease the counter value and check it against
equal to zero.
How is it done in the intro? No special counter there
no, but instead after each pass of the loop it produces
check: whether the sixth bit of register H has become zero (line
139). Let me explain the meaning of this check. Video memory full
(#4000-#57FF) comes from high to low addresses, and in
The HL register pair stores the address of the currently filled cell.
Each time the loop passes, this address is decreased by one.
When all video memory is full, the address is decremented
to #3FFF. Now pay attention to the high byte of the address:
when it lies in the range #40-#57 (%01000000-%01010111), it
the sixth bit is set; but as soon as it becomes equal to #3F
(%00111111), the sixth bit is reset. So it workscondition for exiting the loop. Simple and effective, isn't it?
The general technique for such optimization is as follows:
a special variable is created for the quantity counter
executions of the loop, but instead after each pass
a specific condition associated with an address is checked
data processed in a loop.
Other examples of loops optimized in this way can be found here:
can see in lines 149-180 (sin table calculation),
191-197 (shaping the palette), 238-264 (internal effect loop
"plasma"), 315-326 (screen clearing), 667-677 (writing to registers
AY).
Another example is the main intro loop (lines 213-269).
First of all, we note that it is organized somewhat unusually:
the exit condition is checked in the PLAY subroutine,
responsible for the musical accompaniment (in lines 478-485). Yes
done because the duration of the intro is determined
length of the melody. After such an exit from the cycle, naturally,
it is necessary to remove unnecessary values from the stack (stored in
subroutine contents AF and return address), which is executed
in lines 291-294.
The main loop should exit when it reaches
end of the POSITIONS table. The length of this table (lines 402-430) is not
exceeds 256 bytes. In this case, keep an eye on reaching the end
You can do it very simply - by the low byte of the address of the current element.As soon as it becomes equal to the low byte of the address of the first cell
memory lying after the table is all, the table is processed (rows
483-485). As you can see, this comparison with the transition
takes only five bytes. If the POSITIONS table were longer
256 bytes, then it would contain at least two cells with
identical low bytes of the address. Then I would have to
compare the full address of an element with the address of the first next
memory cells:
LD BC,PLAY
AND A
SBC HL,BC
JR Z,END
And this already takes eight bytes - three bytes longer.
Increased speed due to table specification of functions
────────────────────────── ───────────────────────────
The "plasma" graphic effect used in the intro requires
calculating the sine when calculating each element of the image.
Naturally, in order to have time to calculate the entire image in 1/50
seconds, the sine calculation should be as fast as possible.
The tabular method is most suitable for this purpose. The essence of it
is as follows: there is a table containing values
functions for all possible values of the argument, and when in
the program needs to calculate the value of the function, from the table just
the finished result is taken.
The use of this method, of course, increases the size
program, but at the same time significantly increases performance.
You can take a number from the table in a few clock cycles, and the calculationsine using a ROM calculator (in fact - calculating the sum
series) will take several orders of magnitude longer. In this
it is easy to verify if you notice that the formation of the table when
launching the intro takes as much as 12 seconds, and is calculated at the same time
there are only 256 sine values.
However, be careful when using
table specification of functions on faster processors
compared to the Z80. Make sure first that the winnings are valid
there will be. The fact is that the processor usually runs at higher
frequency than memory, and computing the function may take less
time than the relatively slow reading from memory.
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
26 January 2002
Hello, All!
═══════════════════ start_6 .W ══════════════════
Cycle optimization
──────────────────
You can often benefit from replacing the cycle with advance
a known number of repetitions ("for") per loop with output at
condition ("repeat-until").
Consider, for example, filling the screen with a texture (lines
129-140). The length of the filled area is known in advance - #1800
byte. If we used a "for" loop, we would have to create
a separate counter (and allocate a register pair for it),
enter the number of repetitions into it (#1800), and then in each
pass through the loop, decrease the counter value and check it against
equal to zero.
How is it done in the intro? No special counter there
no, but instead after each pass of the loop it produces
check: whether the sixth bit of register H has become zero (line
139). Let me explain the meaning of this check. Video memory full
(#4000-#57FF) comes from high to low addresses, and in
The HL register pair stores the address of the currently filled cell.
Each time the loop passes, this address is decreased by one.
When all video memory is full, the address is decremented
to #3FFF. Now pay attention to the high byte of the address:
when it lies in the range #40-#57 (%01000000-%01010111), it
the sixth bit is set; but as soon as it becomes equal to #3F
(%00111111), the sixth bit is reset. So it workscondition for exiting the loop. Simple and effective, isn't it?
The general technique for such optimization is as follows:
a special variable is created for the quantity counter
executions of the loop, but instead after each pass
a specific condition associated with an address is checked
data processed in a loop.
Other examples of loops optimized in this way can be found here:
can see in lines 149-180 (sin table calculation),
191-197 (shaping the palette), 238-264 (internal effect loop
"plasma"), 315-326 (screen clearing), 667-677 (writing to registers
AY).
Another example is the main intro loop (lines 213-269).
First of all, we note that it is organized somewhat unusually:
the exit condition is checked in the PLAY subroutine,
responsible for the musical accompaniment (in lines 478-485). Yes
done because the duration of the intro is determined
length of the melody. After such an exit from the cycle, naturally,
it is necessary to remove unnecessary values from the stack (stored in
subroutine contents AF and return address), which is executed
in lines 291-294.
The main loop should exit when it reaches
end of the POSITIONS table. The length of this table (lines 402-430) is not
exceeds 256 bytes. In this case, keep an eye on reaching the end
You can do it very simply - by the low byte of the address of the current element.As soon as it becomes equal to the low byte of the address of the first cell
memory lying after the table is all, the table is processed (rows
483-485). As you can see, this comparison with the transition
takes only five bytes. If the POSITIONS table were longer
256 bytes, then it would contain at least two cells with
identical low bytes of the address. Then I would have to
compare the full address of an element with the address of the first next
memory cells:
LD BC,PLAY
AND A
SBC HL,BC
JR Z,END
And this already takes eight bytes - three bytes longer.
Increased speed due to table specification of functions
────────────────────────── ───────────────────────────
The "plasma" graphic effect used in the intro requires
calculating the sine when calculating each element of the image.
Naturally, in order to have time to calculate the entire image in 1/50
seconds, the sine calculation should be as fast as possible.
The tabular method is most suitable for this purpose. The essence of it
is as follows: there is a table containing values
functions for all possible values of the argument, and when in
the program needs to calculate the value of the function, from the table just
the finished result is taken.
The use of this method, of course, increases the size
program, but at the same time significantly increases performance.
You can take a number from the table in a few clock cycles, and the calculationsine using a ROM calculator (in fact - calculating the sum
series) will take several orders of magnitude longer. In this
it is easy to verify if you notice that the formation of the table when
launching the intro takes as much as 12 seconds, and is calculated at the same time
there are only 256 sine values.
However, be careful when using
table specification of functions on faster processors
compared to the Z80. Make sure first that the winnings are valid
there will be. The fact is that the processor usually runs at higher
frequency than memory, and computing the function may take less
time than the relatively slow reading from memory.
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
26 January 2002
Hello, All!
═══════════════════ start_7 .W ══════════════════
Increased speed by opening loops
───────────────────── ──────────────────────
When it is necessary to optimize a program primarily in terms of length,
this method is rarely used - after all, when opening cycles, the length
programs are increasing. However, in one place the intro is like this
the technique is still used.
Pay attention to how the cleaning procedure works
screen (lines 311-328). First, the impulse is waited
vertical synchronization (HALT command), then set
the border color is black and the attribute buffer (length 768
bytes), and zeroing goes from high to low addresses (so
It’s more convenient to check the loop exit condition). So, reset it
attributes preferably before the ray begins to draw the first
line of the main screen, otherwise it won’t be very pleasant
effect: in one frame the screen will not be completely cleared.
Let's calculate the total operating time of the most optimized
by cycle length (to the right of each command it is indicated how long it will take
clock cycles it is executed):
CLS_1 LD (HL),A ;7
DEC HL ;6
BIT 3,H ;8
JR NZ,CLS_1 ;12
It turns out (7+6+8+12)*768=25344 clock cycles. During this time the beam
will draw more than 1/3 of the screen, and this is not suitable for us.
If there were no limit on the total cycle timeso rigid (say, no more than 24000 cycles), one could
do this: replace the JR command with the faster JP (it is on
byte is longer), which would reduce operating time by
2*768=1536 clock cycles. But in this case such an increase
the speed turns out to be insufficient.
But if you expand the loop, as done in the intro:
CLS_1 LD (HL),A ;7
DEC L ;4
LD(HL),A ;7
DEC HL ;6
BIT 3,H ;8
JR NZ,CLS_1 ;12
then the length will increase by only two bytes, and the execution time
will be (7+4+7+6+8+12)*384=16896 cycles. During this time on the screen
75 lines will have time to be drawn (at 224 cycles per line). This
is already quite acceptable: on the Pentagon (namely on it
demonstrated work on CC 000) upper part of the curb
is 80 lines, so by the time the beam starts
draw the first line of the main screen, the attributes will already be
reset.
Oh, by the way: note that when expanding a loop, it doesn’t just
repeated twice (LD HL,A:DEC HL:LD HL,A:DEC HL). First
the DEC HL command is replaced by the faster DEC L. This is possible
since when it is executed, the high byte HL will certainly not be
change.
Self-modifying code
────────────────────────
How to make a program very simple, fast and short?
Often this can be achieved by making it self-modifyingchanging - changing itself in the process of work. This is one of
the most powerful optimization methods.
In the section "Selecting the most advantageous data representation"
I already mentioned that in each pattern the volume is either
increases, decreases, or remains constant, and that
information about this is in the two most significant bits of the first byte
pattern descriptor. So, in the procedure of playing music when
when moving to the next pattern, the value of these two is checked
bits (see lines 491-508), and according to one of three
possible cases, either the command is entered at the address CODE_I_D_N
INC (HL), or DEC (HL), or NOP (lines 513-514). How easy it is
guess, it is this command that will change the volume during
pattern playing time (see lines 564-567).
If for information on how the volume changes in
current pattern, a separate variable was selected, then
it would be necessary to check its value each time and depending
change the volume from it. This is of course less efficient.
Further, in the same section, I mentioned that patterns in which
two notes are repeated, stored in packaged form, and that
information about pattern packaging is in the fifth bit of the first
byte of its descriptor. When moving to the next pattern, the value
this bit is checked, and in accordance with it at the address
ADR_CODE is placed either the command code NOP (if the patternpacked), or the command code INC HL (otherwise) (see
lines 518-526). What happens next?
Each pattern byte stores information about two notes.
If the pattern is packed, then it contains only one byte with
information about notes; if not, then four bytes. When the next
two notes are played, the command located at address is executed
ADR_CODE (see lines 571-572) and changing the position in the pattern.
If there is NOP, then the position will not change, which means that further
the same two notes are played (as required). If there
command INC HL, then the transition to the next byte will occur
pattern, with information about the next two notes. Easier and not
do, right?
And one last example. On line 629 is the command
JR C_NORM - switch to channel C processing. When the melody
ends, on line 292 in the second byte of this command (where
transition offset is stored) 0 is written. Zero offset
in a command, JR simply means moving on to the next command. So
Thus, instead of outputting three alternating
sound of different frequencies (as was the case throughout the entire melody),
the program will output the same sample as in channels A and B,
and as a result we will hear the final chord.
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
26 January 2002
Hello, All!
═══════════════════ start_7 .W ══════════════════
Increased speed by opening loops
───────────────────── ──────────────────────
When it is necessary to optimize a program primarily in terms of length,
this method is rarely used - after all, when opening cycles, the length
programs are increasing. However, in one place the intro is like this
the technique is still used.
Pay attention to how the cleaning procedure works
screen (lines 311-328). First, the impulse is waited
vertical synchronization (HALT command), then set
the border color is black and the attribute buffer (length 768
bytes), and zeroing goes from high to low addresses (so
It’s more convenient to check the loop exit condition). So, reset it
attributes preferably before the ray begins to draw the first
line of the main screen, otherwise it won’t be very pleasant
effect: in one frame the screen will not be completely cleared.
Let's calculate the total operating time of the most optimized
by cycle length (to the right of each command it is indicated how long it will take
clock cycles it is executed):
CLS_1 LD (HL),A ;7
DEC HL ;6
BIT 3,H ;8
JR NZ,CLS_1 ;12
It turns out (7+6+8+12)*768=25344 clock cycles. During this time the beam
will draw more than 1/3 of the screen, and this is not suitable for us.
If there were no limit on the total cycle timeso rigid (say, no more than 24000 cycles), one could
do this: replace the JR command with the faster JP (it is on
byte is longer), which would reduce operating time by
2*768=1536 clock cycles. But in this case such an increase
the speed turns out to be insufficient.
But if you expand the loop, as done in the intro:
CLS_1 LD (HL),A ;7
DEC L ;4
LD(HL),A ;7
DEC HL ;6
BIT 3,H ;8
JR NZ,CLS_1 ;12
then the length will increase by only two bytes, and the execution time
will be (7+4+7+6+8+12)*384=16896 cycles. During this time on the screen
75 lines will have time to be drawn (at 224 cycles per line). This
is already quite acceptable: on the Pentagon (namely on it
demonstrated work on CC 000) upper part of the curb
is 80 lines, so by the time the beam starts
draw the first line of the main screen, the attributes will already be
reset.
Oh, by the way: note that when expanding a loop, it doesn’t just
repeated twice (LD HL,A:DEC HL:LD HL,A:DEC HL). First
the DEC HL command is replaced by the faster DEC L. This is possible
since when it is executed, the high byte HL will certainly not be
change.
Self-modifying code
────────────────────────
How to make a program very simple, fast and short?
Often this can be achieved by making it self-modifyingchanging - changing itself in the process of work. This is one of
the most powerful optimization methods.
In the section "Selecting the most advantageous data representation"
I already mentioned that in each pattern the volume is either
increases, decreases, or remains constant, and that
information about this is in the two most significant bits of the first byte
pattern descriptor. So, in the procedure of playing music when
when moving to the next pattern, the value of these two is checked
bits (see lines 491-508), and according to one of three
possible cases, either the command is entered at the address CODE_I_D_N
INC (HL), or DEC (HL), or NOP (lines 513-514). How easy it is
guess, it is this command that will change the volume during
pattern playing time (see lines 564-567).
If for information on how the volume changes in
current pattern, a separate variable was selected, then
it would be necessary to check its value each time and depending
change the volume from it. This is of course less efficient.
Further, in the same section, I mentioned that patterns in which
two notes are repeated, stored in packaged form, and that
information about pattern packaging is in the fifth bit of the first
byte of its descriptor. When moving to the next pattern, the value
this bit is checked, and in accordance with it at the address
ADR_CODE is placed either the command code NOP (if the patternpacked), or the command code INC HL (otherwise) (see
lines 518-526). What happens next?
Each pattern byte stores information about two notes.
If the pattern is packed, then it contains only one byte with
information about notes; if not, then four bytes. When the next
two notes are played, the command located at address is executed
ADR_CODE (see lines 571-572) and changing the position in the pattern.
If there is NOP, then the position will not change, which means that further
the same two notes are played (as required). If there
command INC HL, then the transition to the next byte will occur
pattern, with information about the next two notes. Easier and not
do, right?
And one last example. On line 629 is the command
JR C_NORM - switch to channel C processing. When the melody
ends, on line 292 in the second byte of this command (where
transition offset is stored) 0 is written. Zero offset
in a command, JR simply means moving on to the next command. So
Thus, instead of outputting three alternating
sound of different frequencies (as was the case throughout the entire melody),
the program will output the same sample as in channels A and B,
and as a result we will hear the final chord.
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
26 January 2002
Hello, All!
═══════════════════ start_8 .W ══════════════════
Placing Variables Directly in Load Commands
──────────────────────────── ─────────────────────────────
The meaning of this widely used optimization technique is
as follows: instead of allocating separate memory cells for storage
one- and two-byte variables and then loading them from
memory into a register or register pair, variables are allocated
directly in the boot commands. Here is a typical example:
Was: VAR DB 15
......
LD A,(VAR)
......
──────────────────
4 bytes, 13 clock cycles
Became: ......
LD A,15
VAR EQU $-1
......
─────────────────
2 bytes, 7 clock cycles
As we can see, in this case it is possible to reduce both the size of the program,
and the time it takes to complete it. The table below shows
what is the gain depending on the register/register
pairs where data is loaded.
Please note that the Z80 does not have boot commands
bytes from memory to registers B, C, D, E, H, L and index halves
registers Because of this, the program has to, for example, first
load a byte from memory into the accumulator, and then send it
value into the required register (sometimes you also have tostore the accumulator value on the stack and then
restore). Direct loading in these cases gives
special advantage.
╔═══════════════════════╤═══════ ═══════════════════╤═══════════╗
║ Direct │ Loading from memory │ Saving ║
║ loading │ │ ╟
╟───────────┬─────┬─────┼─────── ───────┬─────┬─────┼─────┬─────╢
║ team │ L │ T │ team │ L │ T │ L │ T ║
╠═══════════╪═════╪═════╪═══════ ═══════╪═════╪═════╪═════╪═════╣
║ LD A,N │ 2 │ 7 │ LD A,(addr) │3(+1)│ 13 │ 2 │ 6 ║
╟───────────┼─────┼─────┼─────── ───────┴─────┴─────┼─────┼─────╢
║ LD B,N │ │ │ │ │ ║
║ LD C,N │ │ │ │ │ ║
║ LD D,N │ 2 │ 7 │ no │ >2 │ >6 ║
║ LD E,N │ │ │ │ │ ║
║ LD H,N │ │ │ │ │ ║
║ LD L,N │ │ │ │ │ ║
╟───────────┼─────┼─────┼─────── ───────────────────┼─────┼─────╢
║ LD XH,N │ │ │ │ │ ║
║ LD XL,N │ 3 │ 11 │ no │ >2 │ >6 ║
║ LD YH,N │ │ │ │ │ ║
║ LD YL,N │ │ │ │ │ ║╟───────────┼─────┼─────┼──────────────┬─────┬─────┼─────┼─────╢
║ LD HL,NN │ 3 │ 10 │ LD HL,(addr) │3(+2)│ 16 │ 2 │ 6 ║
╟───────────┼─────┼─────┼──────────────┼─────┼─────┼─────┼─────╢
║ LD DE,NN │ 3 │ 10 │ LD DE,(addr) │4(+2)│ 20 │ 3 │ 10 ║
║ LD BC,NN │ │ │ LD BC,(addr) │ │ │ │ ║
╟───────────┼─────┼─────┼──────────────┼─────┼─────┼─────┼─────╢
║ LD IX,NN │ 4 │ 14 │ LD IX,(addr) │4(+2)│ 20 │ 2 │ 6 ║
║ LD IY,NN │ │ │ LD IY,(addr) │ │ │ │ ║
╟───────────┼─────┼─────┼──────────────┼─────┼─────┼─────┼─────╢
║ LD SP,NN │ 3 │ 10 │ LD SP,(addr) │4(+2)│ 20 │ 3 │ 10 ║
╚═══════════╧═════╧═════╧══════════════╧═════╧═════╧═════╧═════╝
Таблица 1. Сравнение команд загрузки.
L - длина (в байтах), T - время (в тактах). В столбце "Загрузка
из памяти" длина указывается в виде "X(+Y)", где X - длина
команды, а Y - длина загружаемых данных. Экономия указывается с
учетом длины данных.
Примеры применения этого метода в intro вы можете увидеть в
строках 465-466, 478-479, 551-552, 598-600, 602-603, 613-614,
621-623, 641-642 и 650-651.
Если загрузка какой-либо переменной из памяти осуществляется
в нескольких местах программы, то необходимо выбрать, в каком
именно месте она будет заменена на непосредственную загрузку.
При выборе следует руководствоваться тем, чтобы экономия былаmaximum. Let's say, if from several possible replacement places
one is inside the cycle (and otherwise they are equivalent), then
you need to choose it, because with a large number of repetitions
cycle, the time gain will also be large.
Here is an example from the intro:
(1) 448 LD HL,(FREQ_A)
449 LD (FREQ_B),HL
...............................
(2) 598 LD DE,9*256+(freq_g4-freq_shift)
599 FREQ_A EQU $-2 ;E
600 A_AMP EQU $-1 ;D
Why is direct loading used in (2),
and not in (1)? Because in (2) the loading is performed into the register
pair DE, and in (1) - in HL. And, judging by the table
data, the gain will be greatest when replacing the load
in DE (3 bytes/10 clock cycles versus 2 bytes/6 clock cycles).
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
26 January 2002
Hello, All!
═══════════════════ start_8 .W ══════════════════
Placing Variables Directly in Load Commands
──────────────────────────── ─────────────────────────────
The meaning of this widely used optimization technique is
as follows: instead of allocating separate memory cells for storage
one- and two-byte variables and then loading them from
memory into a register or register pair, variables are allocated
directly in the boot commands. Here is a typical example:
Was: VAR DB 15
......
LD A,(VAR)
......
──────────────────
4 bytes, 13 clock cycles
Became: ......
LD A,15
VAR EQU $-1
......
─────────────────
2 bytes, 7 clock cycles
As we can see, in this case it is possible to reduce both the size of the program,
and the time it takes to complete it. The table below shows
what is the gain depending on the register/register
pairs where data is loaded.
Please note that the Z80 does not have boot commands
bytes from memory to registers B, C, D, E, H, L and index halves
registers Because of this, the program has to, for example, first
load a byte from memory into the accumulator, and then send it
value into the required register (sometimes you also have tostore the accumulator value on the stack and then
restore). Direct loading in these cases gives
special advantage.
╔═══════════════════════╤═══════ ═══════════════════╤═══════════╗
║ Direct │ Loading from memory │ Saving ║
║ loading │ │ ╟
╟───────────┬─────┬─────┼─────── ───────┬─────┬─────┼─────┬─────╢
║ team │ L │ T │ team │ L │ T │ L │ T ║
╠═══════════╪═════╪═════╪═══════ ═══════╪═════╪═════╪═════╪═════╣
║ LD A,N │ 2 │ 7 │ LD A,(addr) │3(+1)│ 13 │ 2 │ 6 ║
╟───────────┼─────┼─────┼─────── ───────┴─────┴─────┼─────┼─────╢
║ LD B,N │ │ │ │ │ ║
║ LD C,N │ │ │ │ │ ║
║ LD D,N │ 2 │ 7 │ no │ >2 │ >6 ║
║ LD E,N │ │ │ │ │ ║
║ LD H,N │ │ │ │ │ ║
║ LD L,N │ │ │ │ │ ║
╟───────────┼─────┼─────┼─────── ───────────────────┼─────┼─────╢
║ LD XH,N │ │ │ │ │ ║
║ LD XL,N │ 3 │ 11 │ no │ >2 │ >6 ║
║ LD YH,N │ │ │ │ │ ║
║ LD YL,N │ │ │ │ │ ║╟───────────┼─────┼─────┼──────────────┬─────┬─────┼─────┼─────╢
║ LD HL,NN │ 3 │ 10 │ LD HL,(addr) │3(+2)│ 16 │ 2 │ 6 ║
╟───────────┼─────┼─────┼──────────────┼─────┼─────┼─────┼─────╢
║ LD DE,NN │ 3 │ 10 │ LD DE,(addr) │4(+2)│ 20 │ 3 │ 10 ║
║ LD BC,NN │ │ │ LD BC,(addr) │ │ │ │ ║
╟───────────┼─────┼─────┼──────────────┼─────┼─────┼─────┼─────╢
║ LD IX,NN │ 4 │ 14 │ LD IX,(addr) │4(+2)│ 20 │ 2 │ 6 ║
║ LD IY,NN │ │ │ LD IY,(addr) │ │ │ │ ║
╟───────────┼─────┼─────┼──────────────┼─────┼─────┼─────┼─────╢
║ LD SP,NN │ 3 │ 10 │ LD SP,(addr) │4(+2)│ 20 │ 3 │ 10 ║
╚═══════════╧═════╧═════╧══════════════╧═════╧═════╧═════╧═════╝
Таблица 1. Сравнение команд загрузки.
L - длина (в байтах), T - время (в тактах). В столбце "Загрузка
из памяти" длина указывается в виде "X(+Y)", где X - длина
команды, а Y - длина загружаемых данных. Экономия указывается с
учетом длины данных.
Примеры применения этого метода в intro вы можете увидеть в
строках 465-466, 478-479, 551-552, 598-600, 602-603, 613-614,
621-623, 641-642 и 650-651.
Если загрузка какой-либо переменной из памяти осуществляется
в нескольких местах программы, то необходимо выбрать, в каком
именно месте она будет заменена на непосредственную загрузку.
При выборе следует руководствоваться тем, чтобы экономия былаmaximum. Let's say, if from several possible replacement places
one is inside the cycle (and otherwise they are equivalent), then
you need to choose it, because with a large number of repetitions
cycle, the time gain will also be large.
Here is an example from the intro:
(1) 448 LD HL,(FREQ_A)
449 LD (FREQ_B),HL
...............................
(2) 598 LD DE,9*256+(freq_g4-freq_shift)
599 FREQ_A EQU $-2 ;E
600 A_AMP EQU $-1 ;D
Why is direct loading used in (2),
and not in (1)? Because in (2) the loading is performed into the register
pair DE, and in (1) - in HL. And, judging by the table
data, the gain will be greatest when replacing the load
in DE (3 bytes/10 clock cycles versus 2 bytes/6 clock cycles).
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
26 January 2002
Hello, All!
═══════════════════ start_9 .W ══════════════════
Building tables
─────────────────
If the program uses any tables, then
in many cases its length can be reduced due to the following
simple trick: do not store tables in the body of the program, but
shape them before use. The winnings come from
that the program fragment that builds the table turns out to be
shorter than the table itself.
Of course, we must take into account that the construction of the table
it takes some time, sometimes quite considerable.
However, tables are usually formed only once,
at the very beginning of the program, so it doesn’t have that much
great value.
Here's how this technique is used in the intro.
Sine table (more precisely, table of function values y=[(1+sin
(2*Pi*x/255))*127]) 256 bytes long is created using
32-byte program fragment (lines 145-180). We get
the gain is 224 bytes - however, about 12 are spent on construction
seconds There are, however, faster ones (but also more
long) ways to construct a sine table - if interested,
I recommend that you read [2] (section “Trigonometry”).
The PALETTE table, which contains the attribute values,
also takes 256 bytes. And here is a fragment of the program that builds it
(lines 182-197) takes only 14 bytes, plus 12 bytes for data(lines 274-285) - a total of 26 bytes. How easy it is to calculate
the gain is 230 bytes.
Table with information about what is used during playback
sample music... Here the situation is more interesting. If you
look at the structure of the sample (Fig. 2), you will see that it
can be divided into two parts - the beginning and the looping section. Yes
here, the beginning (the first four elements) is stored in the body
programs (lines 826-829). And the continuation of the sample is completed in
lines 75-116 (25 bytes are spent on this). As a result, the total costs
memory is 29 bytes, and the resulting table SAMPLE
occupies 77 bytes. As you can see, 48 bytes are possible
save.
Using undocumented processor capabilities
──────────────────────────── ─────────────────────────────
In lines 221,223,230,245 you can see
undocumented commands for working with junior and senior
bytes of the 16-bit register IX as with separate 8-bit
registers XL and XH. They are convenient to use when storing
Variables lack regular registers. You can do the same
work with halves of register IY, however, this is not possible in intro
managed to find applications. Although teams working with halves
index registers, one byte longer and 4 clock cycles slower
compared to commands working with conventional 8-bit
registers (due to the prefix byte), their use is stillmore beneficial than storing variables in memory. And, between
by the way, in later modifications of the Z80 processor (Z380 and
etc.) these commands are no longer undocumented.
In lines 666-677 (writing values to AY registers) for
Checking the condition for exiting the loop is used in an undocumented manner.
A special feature of the OUTD command: after its execution, the carry flag
is set if the value of register H has changed and at the same time
the value output to the port is not zero; otherwise flag
reset. Actually, in the process of writing the intro I
discovered this feature - those interested can read about
this in [3].
The cycle is constructed in such a way that writing to the AY registers occurs in
in reverse order - from R10 to R0. After writing to R0 the value of HL
decreases from #8000 to #7FFF. So that the carry flag
turned out to be installed (and this serves as a condition for exiting the cycle),
It is necessary that the value displayed in R0 is not equal to zero.
This value represents the low byte of the frequency divider in
channel A. Throughout the entire melody, this byte is never
becomes equal to zero - this happened, in general, by accident.
If it suddenly turned out that at some moments this byte
resets to zero, then you would have to change the melody - say, by selecting
a different tone.
When using undocumented features,
The question is how will all this work when running the program underemulator. Commands that work with index halves
registers are supported in all emulators known to me, and
about the correct setting of flags when executing a command
OUTD I have no such confidence. However, as it shows
practice, the authors of emulators are constantly improving their
creation, and the apparent inoperability of any
program only serves as an additional incentive to increase
emulation quality. :-)
A few more tricks
───────────────────────
The image in the "plasma" effect moves along a trajectory,
described by equations of the form x=sin(t), y=sin(t+fi). Such
the trajectory is called a Lissajous figure, and its appearance in this case
depends on the value fi - phase difference. If fi=0 or Pi, the figure
degenerates into a straight line segment; if fi=Pi/2 - turns into
a circle, and for other values it is an ellipse
(which is exactly what is needed). In the program fi values from 0 to 2*Pi
correspond to numbers from 0 to #100. Thus, it is clear that
The phase difference can be any, but not a multiple of #40 (i.e.
Pi/2). Taking advantage of this freedom, I chose it equal to #AA -
this allowed on line 227 instead of the ADD A,N command (2 bytes/7
clock cycles) use the ADD A,H command (1 byte/4 clock cycles). The point is
the fact that the H register already contains the number #AA at this point -
high byte of the table address TABLE_SIN. That's how it worked
shorten the program by another byte.The initial value t (located in the XL register) has
influence only on the point from which the movement begins, and the view
the trajectory does not depend on it. So I don't set this value and
This way I don't waste extra bytes. So if you will
run intro at different XL values, then every time
you'll see him a little different. :-)
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
26 January 2002
Hello, All!
═══════════════════ start_9 .W ══════════════════
Building tables
─────────────────
If the program uses any tables, then
in many cases its length can be reduced due to the following
simple trick: do not store tables in the body of the program, but
shape them before use. The winnings come from
that the program fragment that builds the table turns out to be
shorter than the table itself.
Of course, we must take into account that the construction of the table
it takes some time, sometimes quite considerable.
However, tables are usually formed only once,
at the very beginning of the program, so it doesn’t have that much
great value.
Here's how this technique is used in the intro.
Sine table (more precisely, table of function values y=[(1+sin
(2*Pi*x/255))*127]) 256 bytes long is created using
32-byte program fragment (lines 145-180). We get
the gain is 224 bytes - however, about 12 are spent on construction
seconds There are, however, faster ones (but also more
long) ways to construct a sine table - if interested,
I recommend that you read [2] (section “Trigonometry”).
The PALETTE table, which contains the attribute values,
also takes 256 bytes. And here is a fragment of the program that builds it
(lines 182-197) takes only 14 bytes, plus 12 bytes for data(lines 274-285) - a total of 26 bytes. How easy it is to calculate
the gain is 230 bytes.
Table with information about what is used during playback
sample music... Here the situation is more interesting. If you
look at the structure of the sample (Fig. 2), you will see that it
can be divided into two parts - the beginning and the looping section. Yes
here, the beginning (the first four elements) is stored in the body
programs (lines 826-829). And the continuation of the sample is completed in
lines 75-116 (25 bytes are spent on this). As a result, the total costs
memory is 29 bytes, and the resulting table SAMPLE
occupies 77 bytes. As you can see, 48 bytes are possible
save.
Using undocumented processor capabilities
──────────────────────────── ─────────────────────────────
In lines 221,223,230,245 you can see
undocumented commands for working with junior and senior
bytes of the 16-bit register IX as with separate 8-bit
registers XL and XH. They are convenient to use when storing
Variables lack regular registers. You can do the same
work with halves of register IY, however, this is not possible in intro
managed to find applications. Although teams working with halves
index registers, one byte longer and 4 clock cycles slower
compared to commands working with conventional 8-bit
registers (due to the prefix byte), their use is stillmore beneficial than storing variables in memory. And, between
by the way, in later modifications of the Z80 processor (Z380 and
etc.) these commands are no longer undocumented.
In lines 666-677 (writing values to AY registers) for
Checking the condition for exiting the loop is used in an undocumented manner.
A special feature of the OUTD command: after its execution, the carry flag
is set if the value of register H has changed and at the same time
the value output to the port is not zero; otherwise flag
reset. Actually, in the process of writing the intro I
discovered this feature - those interested can read about
this in [3].
The cycle is constructed in such a way that writing to the AY registers occurs in
in reverse order - from R10 to R0. After writing to R0 the value of HL
decreases from #8000 to #7FFF. So that the carry flag
turned out to be installed (and this serves as a condition for exiting the cycle),
It is necessary that the value displayed in R0 is not equal to zero.
This value represents the low byte of the frequency divider in
channel A. Throughout the entire melody, this byte is never
becomes equal to zero - this happened, in general, by accident.
If it suddenly turned out that at some moments this byte
resets to zero, then you would have to change the melody - say, by selecting
a different tone.
When using undocumented features,
The question is how will all this work when running the program underemulator. Commands that work with index halves
registers are supported in all emulators known to me, and
about the correct setting of flags when executing a command
OUTD I have no such confidence. However, as it shows
practice, the authors of emulators are constantly improving their
creation, and the apparent inoperability of any
program only serves as an additional incentive to increase
emulation quality. :-)
A few more tricks
───────────────────────
The image in the "plasma" effect moves along a trajectory,
described by equations of the form x=sin(t), y=sin(t+fi). Such
the trajectory is called a Lissajous figure, and its appearance in this case
depends on the value fi - phase difference. If fi=0 or Pi, the figure
degenerates into a straight line segment; if fi=Pi/2 - turns into
a circle, and for other values it is an ellipse
(which is exactly what is needed). In the program fi values from 0 to 2*Pi
correspond to numbers from 0 to #100. Thus, it is clear that
The phase difference can be any, but not a multiple of #40 (i.e.
Pi/2). Taking advantage of this freedom, I chose it equal to #AA -
this allowed on line 227 instead of the ADD A,N command (2 bytes/7
clock cycles) use the ADD A,H command (1 byte/4 clock cycles). The point is
the fact that the H register already contains the number #AA at this point -
high byte of the table address TABLE_SIN. That's how it worked
shorten the program by another byte.The initial value t (located in the XL register) has
influence only on the point from which the movement begins, and the view
the trajectory does not depend on it. So I don't set this value and
This way I don't waste extra bytes. So if you will
run intro at different XL values, then every time
you'll see him a little different. :-)
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
26 January 2002
Hello, All!
═══════════════════ start_10.W ══════════════════
When playing music, the fact that
the number of notes in the pattern (8) is a power of two. This
allows you to optimize the program fragment where the
increasing the note number and checking to see if the end of the pattern has been reached
(lines 465-471). After the note number is increased, instead of
comparison commands (CP 8) the AND 7 command is used. Both
another command will set the Z flag if after increasing the number
became equal to 8, so for now there seems to be no winning in sight. But after
the AND command will also reset the battery to zero, and
this is exactly what we need, because after the 7th note of one pattern there is
0th note of the next one. That's a win - no special requirement
reset the note number.
See how a 256-byte table is formed
PALETTE. It should consist of 12 equal parts filled
values taken from the 12-byte COLORS table. But 256 by 12
is not completely divisible, and the parts are as follows: 11 by 22 bytes and
one is 14 bytes long.
The filling procedure (lines 191-197) is very simple.
DE contains the address of the beginning of the PALETTE table. From (HL) is taken
next value and written to (DE) 22 times (after writing
each byte E increases). If after recording the next 22
byte reaches the end of the PALETTE table (i.e. E=0), fillingends. If not, then HL is increased and added to the table
The next 22 bytes are entered.
If you trace this procedure, you will find something
far from obvious at first glance. After the table
PALETTE will be filled for the first time, the procedure will not end.
The table will be filled “in a circle” again and again - until
until the last byte of the next 22-byte section matches
with the last byte of the table. By this point, how easy it is
calculate, 2816 bytes will be written to the table (this is the smallest
the common multiple of the numbers 256 and 22), and HL will increase by 2816/22=128.
Therefore, the address set in HL before starting filling
(see line 184) is less than the address of the beginning of the COLORS table, by
128-12=#74 bytes, so after padding is complete,
the PALETTE table will be filled with data from the table
COLORS.
It would seem, why such difficulties - after all, you can simply
add a check for reaching the end of the table in the inner loop.
Yes, you can - but it will take two more bytes of memory.
By the way, similar tables are used when implementing
many graphic effects, so hopefully outlined
the principle of their optimal formation will be useful to someone.
* * *
Well, we didn’t expect that you needed to know so much to
write such a small program? :-) Try your hand atoptimizations - shorten the intro by at least a byte without loss
functionality! I’ll say right away that this is possible and one of the
I know the ways...
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
26 January 2002
Hello, All!
═══════════════════ start_10.W ══════════════════
When playing music, the fact that
the number of notes in the pattern (8) is a power of two. This
allows you to optimize the program fragment where the
increasing the note number and checking to see if the end of the pattern has been reached
(lines 465-471). After the note number is increased, instead of
comparison commands (CP 8) the AND 7 command is used. Both
another command will set the Z flag if after increasing the number
became equal to 8, so for now there seems to be no winning in sight. But after
the AND command will also reset the battery to zero, and
this is exactly what we need, because after the 7th note of one pattern there is
0th note of the next one. That's a win - no special requirement
reset the note number.
See how a 256-byte table is formed
PALETTE. It should consist of 12 equal parts filled
values taken from the 12-byte COLORS table. But 256 by 12
is not completely divisible, and the parts are as follows: 11 by 22 bytes and
one is 14 bytes long.
The filling procedure (lines 191-197) is very simple.
DE contains the address of the beginning of the PALETTE table. From (HL) is taken
next value and written to (DE) 22 times (after writing
each byte E increases). If after recording the next 22
byte reaches the end of the PALETTE table (i.e. E=0), fillingends. If not, then HL is increased and added to the table
The next 22 bytes are entered.
If you trace this procedure, you will find something
far from obvious at first glance. After the table
PALETTE will be filled for the first time, the procedure will not end.
The table will be filled “in a circle” again and again - until
until the last byte of the next 22-byte section matches
with the last byte of the table. By this point, how easy it is
calculate, 2816 bytes will be written to the table (this is the smallest
the common multiple of the numbers 256 and 22), and HL will increase by 2816/22=128.
Therefore, the address set in HL before starting filling
(see line 184) is less than the address of the beginning of the COLORS table, by
128-12=#74 bytes, so after padding is complete,
the PALETTE table will be filled with data from the table
COLORS.
It would seem, why such difficulties - after all, you can simply
add a check for reaching the end of the table in the inner loop.
Yes, you can - but it will take two more bytes of memory.
By the way, similar tables are used when implementing
many graphic effects, so hopefully outlined
the principle of their optimal formation will be useful to someone.
* * *
Well, we didn’t expect that you needed to know so much to
write such a small program? :-) Try your hand atoptimizations - shorten the intro by at least a byte without loss
functionality! I’ll say right away that this is possible and one of the
I know the ways...
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
26 January 2002
Hello, All!
═══════════════════ start_11.W ══════════════════
* * *
A few words about the intro source text below
"Start".
The text is provided with detailed comments. Program lines
are numbered only for ease of reference.
If you need to type a program to compile it and
run, then there is no need to specify line numbers.
The program is designed to be compiled in ZXASM assembler.
If you are using any other assembler, please note
attention to the syntax features characteristic of ZXASM.
Lines 221, 223, 230, 245 contain commands that work with
halves of register pair IX. ZXASM accepts them
designations XH and XL. In other assemblers these halves may
designated as HX and LX, or not supported at all
(after all, this is an undocumented feature of the Z80) - in this
In this case, you will have to correct XH to H, XL to
L, and before each such command put the prefix byte #DD.
For example, the INC XL command on line 221 would look like two
commands: DB #DD and INC L.
In lines 402-430 and 484 you can see the operator "" -
calculating the remainder of division. It is used there to
get the low byte of a double-byte value. If in the used
your assembler does not support this operator, then perhapsa special operator for calculating the low byte is provided
(for example, in ALASM it is "."), or the assembler itself discards
high byte, when the result must be single-byte (by the way, and
ZXASM has this ability). As a last resort, you can
use the equality A256=A-((A/256)*256).
001 ┌─────────────────── ───────────────────┐
002 │ │
003 │ "START" │
004 │ 512 bytes intro │
005 │ for │
006 │ Chaos Constructions 000 │
007 │ │
008 │ (c) 2000 by Ivan Roshin, Moscow │
009 │ │
010 └─────────────────── ───────────────────┘
011
012 ;Data structures:
013
014 TABLE_SIN EQU #AA00 ;this address
015 ;not chosen by chance-
016 ;but!
017
018 PALETTE EQU TABLE_SIN+#100 ;and this one
019 ;too!
020
021 ;Hardware constants of the video controller:
022
023 black EQU 0 ;color numbers
024 blue EQU 1
025 red EQU 2
026 magenta EQU 3
027 green EQU 4
028 cyan EQU 5
029 yellow EQU 6
030 white EQU 7
031
032 paper EQU 8 ;coefficient multiplication
033
034 ;Example of setting an attribute:
035 ;white*paper+black
036
037 border EQU 254 ;control port038 ;border color
039
040 ;Addresses of procedures for work
041 ;with calculator:
042
043 A_TO_STACK EQU #2D28
044 FROM_STACK EQU #2DD5
045
046 ;Calculator commands used:
047
048 multiply EQU #04
049 add_EQU #0F
050 sin EQU #1F
051 stk_data EQU #34
052 end_calc EQU #38
053 stk_one EQU #A1
054
055
056 ;The program itself went:
057
058 ORG #8001
059
060 ;C #8000 hereinafter the first 11 bytes
061 ;will be used as AY dump.
062;
063 ;Currently the following values are set:
064 ;#8007=%00111000 (AY mixer); actually,
065 ;most significant 2 bits are not important and can be
066 ;any, depending on convenience.
067 ;#800A - right channel - 9 (volume
068 ;ornament). The most significant 3 bits can be
069 ;any, depending on convenience.
070;
071 ;Now this data is “built-in”
072 ;program and are interpreted as
073 ;commands.
074
075 ;Build table sample with #8201 (more precisely,
076 ;we complete it - the first 4 values
077 ;already located with #81FD):
078
079 LD HL,#8201
080 LD BC,#0C40
081
082 ;B - current volume (decreases
083; every 6 ints);
084 ;C - constant #40 used for
085; setting the frequency offset in
086; according to up_2, up_0, dn_2.
087
088 ;At address #8007 we have %01111000 - this is
089 ;mixer value and code at the same time
090 ;required here commands LD A,B:
091════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
26 January 2002
Hello, All!
═══════════════════ start_11.W ══════════════════
* * *
A few words about the intro source text below
"Start".
The text is provided with detailed comments. Program lines
are numbered only for ease of reference.
If you need to type a program to compile it and
run, then there is no need to specify line numbers.
The program is designed to be compiled in ZXASM assembler.
If you are using any other assembler, please note
attention to the syntax features characteristic of ZXASM.
Lines 221, 223, 230, 245 contain commands that work with
halves of register pair IX. ZXASM accepts them
designations XH and XL. In other assemblers these halves may
designated as HX and LX, or not supported at all
(after all, this is an undocumented feature of the Z80) - in this
In this case, you will have to correct XH to H, XL to
L, and before each such command put the prefix byte #DD.
For example, the INC XL command on line 221 would look like two
commands: DB #DD and INC L.
In lines 402-430 and 484 you can see the operator "" -
calculating the remainder of division. It is used there to
get the low byte of a double-byte value. If in the used
your assembler does not support this operator, then perhapsa special operator for calculating the low byte is provided
(for example, in ALASM it is "."), or the assembler itself discards
high byte, when the result must be single-byte (by the way, and
ZXASM has this ability). As a last resort, you can
use the equality A256=A-((A/256)*256).
001 ┌─────────────────── ───────────────────┐
002 │ │
003 │ "START" │
004 │ 512 bytes intro │
005 │ for │
006 │ Chaos Constructions 000 │
007 │ │
008 │ (c) 2000 by Ivan Roshin, Moscow │
009 │ │
010 └─────────────────── ───────────────────┘
011
012 ;Data structures:
013
014 TABLE_SIN EQU #AA00 ;this address
015 ;not chosen by chance-
016 ;but!
017
018 PALETTE EQU TABLE_SIN+#100 ;and this one
019 ;too!
020
021 ;Hardware constants of the video controller:
022
023 black EQU 0 ;color numbers
024 blue EQU 1
025 red EQU 2
026 magenta EQU 3
027 green EQU 4
028 cyan EQU 5
029 yellow EQU 6
030 white EQU 7
031
032 paper EQU 8 ;coefficient multiplication
033
034 ;Example of setting an attribute:
035 ;white*paper+black
036
037 border EQU 254 ;control port038 ;border color
039
040 ;Addresses of procedures for work
041 ;with calculator:
042
043 A_TO_STACK EQU #2D28
044 FROM_STACK EQU #2DD5
045
046 ;Calculator commands used:
047
048 multiply EQU #04
049 add_EQU #0F
050 sin EQU #1F
051 stk_data EQU #34
052 end_calc EQU #38
053 stk_one EQU #A1
054
055
056 ;The program itself went:
057
058 ORG #8001
059
060 ;C #8000 hereinafter the first 11 bytes
061 ;will be used as AY dump.
062;
063 ;Currently the following values are set:
064 ;#8007=%00111000 (AY mixer); actually,
065 ;most significant 2 bits are not important and can be
066 ;any, depending on convenience.
067 ;#800A - right channel - 9 (volume
068 ;ornament). The most significant 3 bits can be
069 ;any, depending on convenience.
070;
071 ;Now this data is “built-in”
072 ;program and are interpreted as
073 ;commands.
074
075 ;Build table sample with #8201 (more precisely,
076 ;we complete it - the first 4 values
077 ;already located with #81FD):
078
079 LD HL,#8201
080 LD BC,#0C40
081
082 ;B - current volume (decreases
083; every 6 ints);
084 ;C - constant #40 used for
085; setting the frequency offset in
086; according to up_2, up_0, dn_2.
087
088 ;At address #8007 we have %01111000 - this is
089 ;mixer value and code at the same time
090 ;required here commands LD A,B:
091════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
26 January 2002
Hello, All!
═══════════════════ start_13.W ══════════════════
199 ;PLASMA - register distribution:
200 ;
201 ;DE - address in the attribute file
202; C - y coordinate on screen
203; (the x coordinate is obtained as E AND 31)
204; H - high byte of the sin table address
205; or palettes (differ by 1)
206; L - used to access the table
207; B - x offset
208 ;XH - y offset
209 ;XL - offset in the SIN table (therefore
210; offset takes offsets along x,y),
211; the initial value XL is not important
212
213 PLASMA HALT
214 CALL PLAY
215
216 ;We take x,y offsets from the sin table,
217 ;then the plasma will move along the figure
218 ;Lissajous (in this case, an ellipse):
219
220 LD H,TABLE_SIN/256
221 INC XL
222
223 LD A,XL
224 LD L,A
225 LD B,(HL)
226
227 ADD A,H ;phase shift = #AA
228 LD L,A
229 LD A,(HL)
230 LD XH,A
231
232 LD DE,#5800
233 LD C,24
234 XOR A
235
236 ;sin (2*x+sm_x) + sin (2*y+sm_y) + sm_x
237
238 LOOP ADD A,A ;2*x
239 ADD A,B ;2*x+sm_x
240 LD L,A
241 LD A,(HL) ;sin(2*x+sm_x)
242 EX AF,AF'
243 LD A,C ;y
244 ADD A,A ;2*y
245 ADD A,XH ;2*y+sm_y246 LD L,A
247 EX AF,AF'
248 ADD A,(HL) ;sum of sines
249 ADD A,B ;+sm_x
250 LD L,A
251
252 INC H ;= LD H,PALETTE/256
253
254 LD A,(HL)
255 LD (DE),A
256 INC E
257 LD (DE),A
258 INC DE
259
260 DEC H ;restored H
261
262 LD A,E
263 AND 31
264 JR NZ,LOOP ;loop in x
265
266 DEC C
267 JR NZ,LOOP ;cycle along y
268
269 JR PLASMA ;all over again
270
271 ;---------------------------------------
272 ;Color set for plasma:
273
274 COLORS DB yellow*paper+yellow
275 DB yellow*paper+red
276 DB red*paper+yellow
277 DB red*paper+red
278 DB red*paper+magenta
279 DB magenta*paper+red
280 DB magenta*paper+magenta
281 DB magenta*paper+green
282 DB green*paper+magenta
283 DB green*paper+green
284 DB green*paper+yellow
285 DB yellow*paper+green
286
287 ;---------------------------------------
288 ;Control is passed here when
289 ;melody ends:
290
291 END POP AF ;after this A=0!
292 LD (TRIGGER),A ;offset
293 ;for JR=0294 POP AF ;return address
295
296 CALL CLS
297
298 ;Final chord:
299
300 LD B,#4D
301 ACCORD PUSH BC
302 LD DE,freq_e5-freq_shift
303 CALL NE_NOTE_F
304 HALT
305 POP BC
306 DJNZ ACCORD
307
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
26 January 2002
Hello, All!
═══════════════════ start_13.W ══════════════════
199 ;PLASMA - register distribution:
200 ;
201 ;DE - address in the attribute file
202; C - y coordinate on screen
203; (the x coordinate is obtained as E AND 31)
204; H - high byte of the sin table address
205; or palettes (differ by 1)
206; L - used to access the table
207; B - x offset
208 ;XH - y offset
209 ;XL - offset in the SIN table (therefore
210; offset takes offsets along x,y),
211; the initial value XL is not important
212
213 PLASMA HALT
214 CALL PLAY
215
216 ;We take x,y offsets from the sin table,
217 ;then the plasma will move along the figure
218 ;Lissajous (in this case, an ellipse):
219
220 LD H,TABLE_SIN/256
221 INC XL
222
223 LD A,XL
224 LD L,A
225 LD B,(HL)
226
227 ADD A,H ;phase shift = #AA
228 LD L,A
229 LD A,(HL)
230 LD XH,A
231
232 LD DE,#5800
233 LD C,24
234 XOR A
235
236 ;sin (2*x+sm_x) + sin (2*y+sm_y) + sm_x
237
238 LOOP ADD A,A ;2*x
239 ADD A,B ;2*x+sm_x
240 LD L,A
241 LD A,(HL) ;sin(2*x+sm_x)
242 EX AF,AF'
243 LD A,C ;y
244 ADD A,A ;2*y
245 ADD A,XH ;2*y+sm_y246 LD L,A
247 EX AF,AF'
248 ADD A,(HL) ;sum of sines
249 ADD A,B ;+sm_x
250 LD L,A
251
252 INC H ;= LD H,PALETTE/256
253
254 LD A,(HL)
255 LD (DE),A
256 INC E
257 LD (DE),A
258 INC DE
259
260 DEC H ;restored H
261
262 LD A,E
263 AND 31
264 JR NZ,LOOP ;loop in x
265
266 DEC C
267 JR NZ,LOOP ;cycle along y
268
269 JR PLASMA ;all over again
270
271 ;---------------------------------------
272 ;Plasma color set:
273
274 COLORS DB yellow*paper+yellow
275 DB yellow*paper+red
276 DB red*paper+yellow
277 DB red*paper+red
278 DB red*paper+magenta
279 DB magenta*paper+red
280 DB magenta*paper+magenta
281 DB magenta*paper+green
282 DB green*paper+magenta
283 DB green*paper+green
284 DB green*paper+yellow
285 DB yellow*paper+green
286
287 ;---------------------------------------
288 ;Control is passed here when
289 ;melody ends:
290
291 END POP AF ;after this A=0!
292 LD (TRIGGER),A ;offset
293 ;for JR=0294 POP AF ;return address
295
296 CALL CLS
297
298 ;Final chord:
299
300 LD B,#4D
301 ACCORD PUSH BC
302 LD DE,freq_e5-freq_shift
303 CALL NE_NOTE_F
304 HALT
305 POP BC
306 DJNZ ACCORD
307
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
26 January 2002
Hello, All!
═══════════════════ start_14.W ══════════════════
308 ;---------------------------------------
309 ;Screen cleaning procedure:
310
311 CLS XOR A
312 OUT (border),A
313
314 LD HL,#5AFF
315 CLS_1 LD (HL),A
316 DEC L
317
318 ;Expand the loop - the code is 2 longer
319 ;bytes, but when calling CLS after HALT
320 ;attributes will be cleared before they enter
321 ;under beam.
322
323 LD (HL),A
324 DEC HL
325 BIT 3.H
326 JR NZ,CLS_1
327
328 RET
329
330 END_MAIN
331
332 ;---------------------------------------
333
334 ORG #80C6
335
336 ███ Player
337
338 ▒▒▒ Constants
339
340 tempo EQU 12 ;interval between
341 ;notes in 1/50 s
342
343 length EQU 24 ;melody length
344 ;in patterns
345
346 patt_size EQU 8 ;number of notes
347 ;in pattern
348
349 note_g4 EQU 0 ;note numbers
350 note_b4 EQU 1 ;(not by chance notes
351 note_a4 EQU 2 ;go in this
352 note_c5 EQU 3 ;okay - this is
353 note_e5 EQU 4 ;allows combination
354 note_g5 EQU 5 ;read tables
355 note_b5 EQU 6 ;FREQ_TABLE and
356 note_d5 EQU 7 ;ORNAMENTS)
357 note_f5 EQU 8
358 note_a5 EQU 9
359
360 freq_g4 EQU #114 ;divisor values361 freq_a4 EQU #F8 ;frequencies for
362 freq_b4 EQU #DD ;each
363 freq_c5 EQU #CF ;notes used
364 freq_d5 EQU #B8 ;
365 freq_e5 EQU #A5 ;(used
366 freq_f5 EQU #9B ;pure gamma, and
367 freq_g5 EQU #8A ;not tempered-
368 freq_a5 EQU #7C ;naya)
369 freq_b5 EQU #6E
370
371 freq_shift EQU freq_g4-#C9
372 ;such a meaning,
373 ;so that for each
374 ;dogo freq number
375 ;freq-freq_shift
376 ;was 0..#FF
377
378 code_up EQU #00 ;with these values
379 code_dn EQU #40 ;coded, will
380 code_max EQU #80 ;is the volume in
381 ;pattern raise-
382 ;sya, go down or
383 ;to be maximum
384
385 code_pack EQU #20 ;packed pattern
386 ;(2 notes repeated
387 ;4 times)
388
389 ornament_0 EQU 1 ;offsets used
390 ornament_1 EQU 5;
391 ornament_2 EQU 2 ;ORNAMENTS table
392 ornament_3 EQU 0
393
394
395 ▒▒▒ Variables
396
397 ;Table positions
398;
399 ;The low byte of the beginning of each
400 ;pattern.
401
402 POSITIONS DB PATTERN_0256
403 DB PATTERN_1256
404 DB PATTERN_2256
405 DB PATTERN_3256
406407 DB PATTERN_0256
408 DB PATTERN_1256
409 DB PATTERN_2256
410 DB PATTERN_4256
411
412 DB PATTERN_5256
413 DB PATTERN_6256
414 DB PATTERN_2256
415 DB PATTERN_3256
416
417 DB PATTERN_5256
418 DB PATTERN_7256
419 DB PATTERN_8256
420 DB PATTERN_9256
421
422 DB PATTERN_10256
423 DB PATTERN_11256
424 DB PATTERN_12256
425 DB PATTERN_12256
426
427 DB PATTERN_10256
428 DB PATTERN_11256
429 DB PATTERN_12256
430 DB PATTERN_12256
431
════════════════════════════════════════════════
С уважением, Иван Рощин.
From
Ivan Roshin
→
To
All
26 January 2002
Hello, All!
═══════════════════ start_15.W ══════════════════
432 ▒▒▒ Procedures
433
434 PLAY
435
436 ;Checking: we need to move on to the next one
437 ;note?
438
439 LD HL,QUARK
440 DEC (HL)
441 JR NZ,NE_NOTE ;no
442
443 LD (HL),tempo
444
445 ;When moving, we immediately copy the values
446 ;frequency and volume from channel A to B:
447
448 LD HL,(FREQ_A)
449 LD (FREQ_B),HL
450
451 ;And set the position to SAMPLE for
452 ;channel A:
453
454 LD HL,SAMPLE
455 LD (POS_IN_S_A),HL
456
457 ;But this installation is only useful
458 ;at the end...
459
460 LD (POS_IN_S_B),HL
461
462 ;Check: need to move on to the next one
463 ;position?
464
465 LD A,patt_size-1
466 NOTE EQU $-1
467 INC A
468 AND 7
469 PUSH AF ;remembered!
470 LD (NOTE),A
471 JR NZ,NE_POSIT
472
473 ;Move to next position:
474
475 LD D,A ;=LD DE,0
476 LD E,A ;(useful later)
477
478 LD HL,POSITIONS-1
479 POSITION EQU $-2
480 INC HL
481 LD (POSITION),HL
482
483 LD A,L
484 CP PLAY256
485 JR Z,END
486
487 LD L,(HL)
488 INC H ;=LD H,PATTERN_0/256
489 LD A,(HL)
490491 ;In the two most significant bits of the accumulator
492 ;the value characterizing
493 ;change volume in pattern:
494 ;%00 for up, %10 for dn, %01 for max.
495 ;Depending on this value
496 ;set to address CODE_I_D_N
497 ;one of the commands INC (HL), DEC (HL), NOP,
498 ;and also set the current volume
499 ;ALL_AMP
500
501 ADD A,A ;code_max=#80
502 JR C,SET_PAR ;with DE=#0000
503
504 LD DE,#34FF
505 ADD A,A ;code_dn=#40
506 JR C,SET_PAR
507
508 LD DE,#3505 ;code_up=#00
509
510 ;Now in D is what needs to be written down
511 ;in CODE_I_D_N, in E - ALL_AMP.
512
513 SET_PAR LD A,D
514 LD (CODE_I_D_N),A
515 LD A,E
516 LD (A_AMP),A
517
518 ;If the pattern is packed (i.e. in the pattern
519 ;2 notes are repeated), place the code
520 ;NOP (#00), otherwise the code is INC HL (#23):
521
522 BIT 5,(HL)
523 LD A,#23
524 JR Z,SET_COD
525 XOR A
526 SET_COD LD (ADR_CODE),A
527
528 ; Low 5 bits - ornament number
529
530 LD A,(HL)
531 AND %00011111
532 LD (ORNAMENT),A
533
534 ;Set the pattern start address:
535
536 INC HL
537 LD (POS_IN_PAT),HL
538
════════════════════════ ════════════════════════Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
26 January 2002
Hello, All!
═══════════════════ start_16.W ══════════════════
539 ;Processing another NOTE.
540 ;Place a new position indicator
541 ;in sample for channel B (for A there was
542 ;placed earlier):
543
544 NE_POSIT LD HL,SAMPLE+12
545 LD (POS_IN_S_B),HL
546
547 ;Here we will take the older ones one by one
548 ;4 bits or low: if low. NOTE bit
549 ;=0, then high, if 1, then low.
550
551 LD HL,0
552 POS_IN_PAT EQU $-2
553
554 POP AF ;NOTE
555 RRCA
556 LD A,(HL)
557 JR C,NOTE_LOW
558
559 RRCA
560 RRCA
561 RRCA
562 RRCA
563
564 ;Changing the volume:
565
566 LD HL,A_AMP
567 CODE_I_D_N NOP ;or INC (HL), DEC (HL)
568
569 JR NOTE_TO_AY
570
571 NOTE_LOW INC HL ;or NOP for PACKED
572 ADR_CODE EQU $-1
573 LD (POS_IN_PAT),HL
574
575 NOTE_TO_AY AND %00001111
576 ADD A,FREQ_TABLE256
577 LD L,A
578
579 ;FREQ_TABLE/256 is already set in H
580
581 LD A,(HL)
582 LD (FREQ_A),A
583
584 NE_NOTE
585
586 ;Processing channel A (central):
587
588 ;At the very beginning, channel A contains the note g4
589 ;with volume -9 (see below). At the first
590 ;calling PLAY these values will be
591 ;copied for channel B, and there will be
592 ;quiet (amplitude 2->1->0) sound this593 ; note (exactly this one - because further it
594; used there). Actually on the channel
595 ;B at the very beginning there should be silence -
596 ;but it’s easier to do this:
597
598 LD DE,9*256+(freq_g4-freq_shift)
599 FREQ_A EQU $-2 ;E
600 A_AMP EQU $-1 ;D
601
602 NE_NOTE_F LD HL,0
603 POS_IN_S_A EQU $-2
604 LD A,(HL)
605 INC HL
606 LD (POS_IN_S_A),HL
607
608 LD L.8
609 CALL IZM_FRQ
610
611 ;Processing channel B (left):
612
613 LD HL,0
614 POS_IN_S_B EQU $-2
615 LD A,(HL)
616 PUSH AF ;for fin. chord
617 INC HL
618 LD (POS_IN_S_B),HL
619
620 LD L.9
621 LD DE,0
622 FREQ_B EQU $-2 ;E
623 B_AMP EQU $-1 ;D
624 CALL IZM_FRQ
625 POP AF
626
627 ;Processing channel C (right):
628
629 JR C_NORM
630 TRIGGER EQU $-1
631
632 ;At the end of the melody (final chord)
633 ;0 is written to TRIGGER, which means
634 ;go to next command.
635
636 LD L,#A
637 LD E,freq_g5-freq_shift
638 CALL IZM_FRQ
639 JR OUT_AY
640
641 C_NORM LD A,2 ;past
642 SM_IN_ORN EQU $-1 ;offset
643;in orn.
644 INC A
645 CP 3
646 JR NZ,SAVE_SM647 XOR A
648 SAVE_SM LD (SM_IN_ORN),A
649
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
26 January 2002
Hello, All!
═══════════════════ start_17.W ══════════════════
650 ADD A,ornament_0 ;N current orn
651 ORNAMENT EQU $-1
652 ADD A,ORNAMENTS256
653
654 LD H,ORNAMENTS/256
655 LD L,A
656
657 ;HL indicates the value of the divisor
658 ;frequencies.
659
660 LD A,(HL)
661 LD HL,#8004
662 CALL IZM_FRQ_2
663
664 ;Output dump AY:
665
666 OUT_AY LD L,#0A ;H=#80
667 OUT_AY_1 LD BC,#FFFD
668 OUT (C),L
669 LD B,#BF
670 OUTD
671
672 ;After OUTD flag C=0, if the value of H is not
673 ;changed after reduction, and C=1,
674 ;if changed (and if (HL) is not 0).
675 ;You didn’t know? ;-)
676
677 JR NC,OUT_AY_1
678 RET
679
680 ;---------------------------------------
681
682 IZM_FRQ SUB D
683 LD H,#80
684 LD (HL),A ;volume
685
686 ;Now in register L register number AY
687 ;for the channel amplitude, transform it
688 ;to register number AY for low byte
689 ;frequencies of the same channel:
690
691 SLA L ;*2
692 RES 4,L ;-16
693
694 RLCA
695 RLCA
696 RLCA
697 AND 7
698ADD A,E
699 SUB 2
700
701 IZM_FRQ_2 ADD A,freq_shift
702
703 ;After adding freq_shift to A there will be704 ;low byte of the divider, and in flag C -
705 ;high byte.
706
707 LD (HL),A
708
709 INC L
710 LD A,0 ;get the value
711 ADC A,A ;high byte
712 ;and output it
713 LD (HL),A
714
715; RET (see below)
716
717 ;Frequency table (each note in the table
718 ;corresponds to the value of the frequency divider
719 ;minus the offset freq_shift, i.e. for one
720 ;note one byte is spent, not two).
721;
722 ;The value of freq_shift is chosen as follows
723 ;so that the first byte of the table
724 ;equal to #C9 and served simultaneously
725 ;by the RET command, which reduces the total
726 ;program length.
727;
728 ;freq_shift min = #15
729 ;freq_shift max = #6E
730;
731 ;1st digit in table (g4): m.b. #A6-#FF
732;
733 ;Combined with the ORNAMENTS table.
734;
735 ;Ornaments table (8 bytes)
736;
737 ;Freq values are stored. For use
738 ;you need to know the offset in this table
739 ;tse, then ornament is 3 bytes, from
740 ;read from this offset.
741
742 FREQ_TABLE DB freq_g4-freq_shift
743 DB freq_b4-freq_shift
744 ORNAMENTS DB freq_a4-freq_shift
745 DB freq_c5-freq_shift
746 DB freq_e5-freq_shift
747 DB freq_g5-freq_shift
748 DB freq_b5-freq_shift
749 DB freq_d5-freq_shift
750 DB freq_f5-freq_shift751 DB freq_a5-freq_shift
752
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
26 January 2002
Hello, All!
════════════════════ start_18.W
753 ;Table not in every pattern
754;
755 ;1 byte - volume + ornament
756 ; 4 bytes - notes, in bytes 2 notes
757
758 PATTERN_0 DB code_dn+code_pack+ornament_0
759 DB note_c5*16+note_g4
760
761 PATTERN_1 DB code_dn+code_pack+ornament_1
762 DB note_d5*16+note_a4
763
764 PATTERN_2 DB code_dn+code_pack+ornament_0
765 DB note_e5*16+note_b4
766
767 PATTERN_3 DB code_up+code_pack+ornament_0
768 DB note_e5*16+note_b4
769
770 PATTERN_4 DB code_up+ornament_0
771 DB note_e5*16+note_c5
772 DB note_d5*16+note_e5
773 DB note_a5*16+note_g5
774 DB note_f5*16+note_e5
775
776 PATTERN_5 DB code_dn+code_pack+ornament_1
777 DB note_a5*16+note_d5
778
779 PATTERN_6 DB code_max+code_pack+ornament_2
780 DB note_g5*16+note_d5
781
782 PATTERN_7 DB code_dn+code_pack+ornament_2
783 DB note_b5*16+note_e5
784
785 PATTERN_8 DB code_dn+code_pack+ornament_0
786 DB note_f5*16+note_c5
787
788 PATTERN_9 DB code_up+ornament_0
789 DB note_g5*16+note_c5
790 DB note_g5*16+note_c5
791 DB note_a5*16+note_c5
792 DB note_a5*16+note_c5
793
794 PATTERN_10 DB code_max+ornament_1795 DB note_a5*16+note_d5
796 DB note_f5*16+note_d5
797 DB note_a5*16+note_d5
798 DB note_f5*16+note_d5
799
800 PATTERN_11 DB code_max+ornament_3
801 DB note_e5*16+note_a4
802 DB note_c5*16+note_a4
803 DB note_e5*16+note_a4
804 DB note_c5*16+note_a4
805
806 PATTERN_12 DB code_max+ornament_0
807 DB note_g5*16+note_c5
808 DB note_e5*16+note_c5
809 DB note_g5*16+note_c5
810 DB note_e5*16+note_c5
811
812 END_PLAY
813
814 ORG #81FD
815
816 ;Constants added to a value
817 ;amplitudes in sample and indicating
818 ;frequency offset:
819
820 up_2 EQU #80
821 up_0 EQU #40
822 dn_2 EQU #00
823
824 ;First 4 bytes of table sample:
825
826 SAMPLE DB #F+up_0
827 DB #E+up_0
828 DB #D+up_0
829 DB #D+up_0
Literature
──────────
1. I. Roshchin. "Z80: optimization of loading constants into registers."
"Radio amateur. Your computer" 9/2000, 2/2001 (under
alias BV_Creator).
2. I. Roshchin. "More about programming arithmetic operations."
"Radio amateur. Your computer" 12/2000, 1-3/2001.
3. I. Roshchin. "The effect of the OUTD command on the carry flag."
"Radio Amateur. Your Computer" 5/2001 (under the pseudonym
BV_Creator).════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
26 January 2002
Hello, All!
═══════════════════ start_12.W ══════════════════
092 MAKE_SAMPL LD A,B
093 LD (HL),B
094 INC L
095
096 ;At address #800A - #A9 (volume
097 ;ornament in the right channel = 9) and
098 ;simultaneously the code of the command needed here
099 ;XOR C:
100
101 XOR C
102 LD (HL),A
103 INC L
104 LD (HL),A
105 INC L
106ADD A,C
107 LD (HL),A
108 INC L
109 SUB C
110 LD (HL),A
111 INC L
112 LD (HL),A
113 INC L
114 DJNZ MAKE_SAMPL
115
116 LD (HL),B ;=LD (HL),0
117
118 HALT
119 CALL CLS
120
121 ;Draw the mesh (HL=#57FF):
122
123 ;This procedure is specifically written this way,
124 ;so that there is a fragment JR $+3, because the offset
125 ;in JR equal to 1 will be used
126 ;then as data (variable
127 ;QUARK).
128
129 GRID_1 LD D,%10101010
130 BIT 0.H
131 JR Z,GRID_2
132 LD DE,%0001000101000100
133 BIT 1,H
134 JR Z,GRID_2
135 QUARK EQU $-1 ;this byte is 1
136 LD D,E
137 GRID_2 LD (HL),D
138 DEC HL
139 BIT 6,H
140 JR NZ,GRID_1
141
142 ;After drawing the mesh HL=#3FFF, D=#AA,
143 ;A=0 (this is still after CLS)!
144145 ;We form a sin table:
146
147 LD E,A ;DE=#AA00=TABLE_SIN!
148
149 LOOP_SIN PUSH DE
150
151 ;If you use register pair BC
152 ;instead of DE, you can save a byte
153 ;(instead of LD A,C: CALL A_TO_STACK we put
154 ;CALL A_TO_STACK+1). Unfortunately, in this
155 ;program to use BC instead of DE
156 ;it’s impossible (otherwise we’ll lose more later)...
157
158 LD A,E
159 CALL A_TO_STACK
160
161 ;Calculation
162 ;int ((1+sin((2*Pi/255)*COUNTER))*127)
163
164 RST #28
165 DB stk_data ;2*Pi/255
166 DB #EB,#49,#D9,#B4,#56
167 DB multiply
168 DB sin
169 DB stk_one
170 DB add_
171 DB stk_data ;127
172 DB #40,#B0,#00,#7F
173 DB multiply
174 DB end_calc
175
176 CALL FROM_STACK
177 POP DE
178 LD (DE),A
179 INC E
180 JR NZ,LOOP_SIN
181
182 ;Form the palette (DE=TABLE_SIN!):
183
184 LD HL,COLORS-#74
185
186 ;Offset -#74 is selected specifically.
187 ;And all because 256 is not divisible by 12.
188
189 INC D ;DE:=PALETTE!
190
191 MAKE_PAL_1 LD A,(HL)
192 INC HL
193 LD B,0+(256+11)/12
194 MAKE_PAL_2 LD (DE),A
195 INC E
196 DJNZ MAKE_PAL_2
197 JR NZ,MAKE_PAL_1
198════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
26 January 2002
Hello, All!
═══════════════════ start_12.W ══════════════════
092 MAKE_SAMPL LD A,B
093 LD (HL),B
094 INC L
095
096 ;At address #800A - #A9 (volume
097 ;ornament in the right channel = 9) and
098 ;simultaneously the code of the command needed here
099 ;XOR C:
100
101 XOR C
102 LD (HL),A
103 INC L
104 LD (HL),A
105 INC L
106ADD A,C
107 LD (HL),A
108 INC L
109 SUB C
110 LD (HL),A
111 INC L
112 LD (HL),A
113 INC L
114 DJNZ MAKE_SAMPL
115
116 LD (HL),B ;=LD (HL),0
117
118 HALT
119 CALL CLS
120
121 ;Draw the mesh (HL=#57FF):
122
123 ;This procedure is specifically written this way,
124 ;so that there is a fragment JR $+3, because the offset
125 ;in JR equal to 1 will be used
126 ;then as data (variable
127 ;QUARK).
128
129 GRID_1 LD D,%10101010
130 BIT 0.H
131 JR Z,GRID_2
132 LD DE,%0001000101000100
133 BIT 1,H
134 JR Z,GRID_2
135 QUARK EQU $-1 ;this byte is 1
136 LD D,E
137 GRID_2 LD (HL),D
138 DEC HL
139 BIT 6,H
140 JR NZ,GRID_1
141
142 ;After drawing the mesh HL=#3FFF, D=#AA,
143 ;A=0 (this is still after CLS)!
144145 ;We form a sin table:
146
147 LD E,A ;DE=#AA00=TABLE_SIN!
148
149 LOOP_SIN PUSH DE
150
151 ;If you use register pair BC
152 ;instead of DE, you can save a byte
153 ;(instead of LD A,C: CALL A_TO_STACK we put
154 ;CALL A_TO_STACK+1). Unfortunately, in this
155 ;program to use BC instead of DE
156 ;it’s impossible (otherwise we’ll lose more later)...
157
158 LD A,E
159 CALL A_TO_STACK
160
161 ;Calculation
162 ;int ((1+sin((2*Pi/255)*COUNTER))*127)
163
164 RST #28
165 DB stk_data ;2*Pi/255
166 DB #EB,#49,#D9,#B4,#56
167 DB multiply
168 DB sin
169 DB stk_one
170 DB add_
171 DB stk_data ;127
172 DB #40,#B0,#00,#7F
173 DB multiply
174 DB end_calc
175
176 CALL FROM_STACK
177 POP DE
178 LD (DE),A
179 INC E
180 JR NZ,LOOP_SIN
181
182 ;Form the palette (DE=TABLE_SIN!):
183
184 LD HL,COLORS-#74
185
186 ;Offset -#74 is selected specifically.
187 ;And all because 256 is not divisible by 12.
188
189 INC D ;DE:=PALETTE!
190
191 MAKE_PAL_1 LD A,(HL)
192 INC HL
193 LD B,0+(256+11)/12
194 MAKE_PAL_2 LD (DE),A
195 INC E
196 DJNZ MAKE_PAL_2
197 JR NZ,MAKE_PAL_1
198════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.