Skip to content

Commit

Permalink
Convert some more bit tables
Browse files Browse the repository at this point in the history
Recovered from a very old stash, hopefully I "rebased" correctly.
Since this is changing the structure of the bit explanations,
some modifications to the content have been made as well.
  • Loading branch information
ISSOtm committed Jul 1, 2023
1 parent 002353f commit 23df373
Show file tree
Hide file tree
Showing 10 changed files with 253 additions and 135 deletions.
28 changes: 28 additions & 0 deletions DEPLOY.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,34 @@ Note that the angle brackets [are only required if there are spaces in the URL](
In effect, this means that linking to a section is as simple as copy-pasting its name in the URL field, prepending a `#`, and wrapping everything in `<>` if the name contains a space.
### Bit descriptions
```markdown
{{#bits 8 > SB 7-0:"Serial data"; SC 7:"Transfer start" 1:"Clock speed" 0:"Clock source"}}
```
Pan Docs describes a lot of hardware registers, and [it has been agreed upon that tables are the best format for this](https://github.com/gbdev/pandocs/issues/318).
However, the best formatting requires `colspan`, which requires HTML tables, which are quite tedious to write; hence, a shorthand syntax was developed.
This is typically used for bit descriptions (hence the name), but is generic enough to work e.g. for byte descriptions as well.
The first argument is the number of columns (not counting the title one).
The second argument is the direction of the indices: `<` for increasing, `>` for decreasing.
Decreasing is preferred for bit descriptions, and increasing for byte descriptions.
The following arguments can be repeated for as many rows as desired, separated by semicolons `;`:
- One argument (which may not start with a digit) names the row; if exactly "\_", it will be ignored.
- Any amount of arguments (even zero) name the individual fields, which must be ordered as in the example. Fields may span several bits, as shown above.
Note: these are usually followed by more detailed descriptions of the fields.
The format of those is documented [in the style guide](https://github.com/gbdev/pandocs/wiki/Document-Style#ANCHOR_FOR_ADDITION_BELOW).
\[THIS WILL NOT BE ADDED TO THE PR, BUT TO THE STYLE GUIDE ON THE WIKI. IT'S MERELY HERE FOR REVIEW.\]
The format of bit description lists is as follows: `- **Field name** [*Additional notes*] (*Read/Write*): Description`.
Additional notes are, for example, to note CGB exclusivity; they are not required.
The "Read/Write" part may be omitted if all fields within the byte are readable and writable; otherwise, it must be indicated for all fields, and both words must be fully spelled out, or spelled exactly "Read-only"/"Write-only".
## Syntax highlighting
Syntax highlighting is provided within the browser, courtesy of [`highlight.js`](https://github.com/highlightjs/highlight.js).
Expand Down
10 changes: 10 additions & 0 deletions custom/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,13 @@ math[display="block"], mfrac {
mfrac msup :not(:first-child) {
font-size: 1.8rem;
}


table.bit-descrs th {
padding: 3px 10px;
}

/* mdBook aligns the first column, but this is not desirable for those tables */
table.bit-descrs.nameless td:first-child {
text-align: center;
}
166 changes: 130 additions & 36 deletions preproc/src/preproc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ use mdbook::errors::Error;
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
use pulldown_cmark::{CowStr, Event, LinkType, Options, Parser, Tag};
use regex::Regex;
use std::borrow::Cow;
use std::collections::HashMap;
use std::io::Write;
use std::iter;
use std::process::{Command, Stdio};
use std::str;
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
Expand Down Expand Up @@ -325,47 +327,69 @@ impl Pandocs {
let mut replaced = String::with_capacity(chapter.content.len());

for result in find_bit_descrs(&chapter.content) {
let (start, end, attrs) = result?;
let (cap_start, cap_end, attrs) = result?;

replaced.push_str(&chapter.content[previous_end_index..start]);
replaced.push_str("<table><thead><tr><th></th>");
for i in (0..attrs.width).rev() {
replaced.push_str(&chapter.content[previous_end_index..cap_start]);

let (start, end) = if attrs.increasing {
(0, attrs.width - 1)
} else {
(attrs.width - 1, 0)
};

// Generate the table head
if !attrs.rows[0].0.is_empty() {
// If names are present, add an empty cell for the name column
replaced.push_str("<table class=\"bit-descrs\"><thead><tr><th></th>");
} else {
// Otherwise, add a class to force correct styling of first column
replaced.push_str("<table class=\"bit-descrs nameless\"><thead><tr>");
}
// Start at `start`, and step each time
for i in iter::successors(Some(start), |i| {
(*i != end).then(|| if attrs.increasing { i + 1 } else { i - 1 })
}) {
replaced.push_str(&format!("<th>{}</th>", i));
}
replaced.push_str("</tr></thead><tbody>");

for (name, row) in &attrs.rows {
replaced.push_str(&format!("<tr><td><strong>{}</strong></td>", name));
let mut pos = attrs.width;
replaced.push_str("<tr>");
// If a name is present, add it
if !name.is_empty() {
replaced.push_str(&format!("<td><strong>{}</strong></td>", name));
}
let mut pos = 0;
let mut fields = row.iter().peekable();
while pos != 0 {
let (start, unused, name) = match fields.peek() {
while pos < attrs.width {
let (len, is_unused, name) = match fields.peek() {
// If we are at the edge of a "used" field, use it
Some(field) if field.end == pos - 1 => (field.start, false, field.name),
Some(field) if field.start == pos => (field.len, false, field.name),
// If in an unused field, end at the next field, or the width if none such
res => (res.map_or(0, |field| field.end + 1), true, ""),
res => (res.map_or(attrs.width, |field| field.start) - pos, true, ""),
};

replaced.push_str(&format!(
"<td colspan=\"{}\"{}>{}</td>",
pos - start,
if unused {
len,
if is_unused {
" class=\"unused-field\""
} else {
""
},
name
));

if !unused {
if !is_unused {
fields.next();
}
pos = start;
pos += len;
}
replaced.push_str("</tr>");
}
replaced.push_str("</tbody></table>");

previous_end_index = end;
previous_end_index = cap_end;
}

replaced.push_str(&chapter.content[previous_end_index..]);
Expand Down Expand Up @@ -394,17 +418,20 @@ fn find_bit_descrs(
.map(|caps| {
// Must use `.get()`, as indexing ties the returned value's lifetime to `caps`'s.
let contents = caps.get(1).unwrap().as_str();
BitDescrAttrs::from_str(contents).map(|attrs| {
let all = caps.get(0).unwrap(); // There is always a 0th capture.
(all.start(), all.end(), attrs)
})
BitDescrAttrs::from_str(contents)
.map(|attrs| {
let all = caps.get(0).unwrap(); // There is always a 0th capture.
(all.start(), all.end(), attrs)
})
.context(format!("Failed to parse \"{contents}\""))
})
}

#[derive(Debug)]
struct BitDescrAttrs<'input> {
width: usize,
rows: Vec<(&'input str, Vec<BitDescrField<'input>>)>,
increasing: bool,
}

impl<'input> BitDescrAttrs<'input> {
Expand All @@ -421,8 +448,34 @@ impl<'input> BitDescrAttrs<'input> {
))?;
let s = contents[width_len..].trim_start();

// Then, parse the direction
let mut chars = s.chars();
// Angle brackets have a tendency to get escaped, so account for that
let (base_len, next) = match chars.next() {
Some('\\') => ('\\'.len_utf8(), chars.next()),
c => (0, c),
};
let increasing = match next {
Some('<') => true,
Some('>') => false,
c => {
bail!(
"Expected width to be followed by '<' or '>' for direction, found {}",
c.map_or(Cow::from("nothing"), |c| format!(
"'{}'",
&s[..base_len + c.len_utf8()]
)
.into()),
);
}
};
debug_assert_eq!('<'.len_utf8(), 1);
debug_assert_eq!('>'.len_utf8(), 1);
let s = s[base_len + 1..].trim_start();

// Next, parse the rows!
let mut rows = Vec::new();
let mut name_type = None;
for row_str in s.split_terminator(';') {
let row_str = row_str.trim();

Expand Down Expand Up @@ -452,45 +505,86 @@ impl<'input> BitDescrAttrs<'input> {
let Some(cap) = RE.captures(row_str) else {
bail!("Failed to parse field for \"{}\"", row_str);
};
let end = cap[1].parse().unwrap();
let start = cap
let left: usize = cap[1].parse().unwrap();
let right = cap
.get(2)
.map_or(end, |end_match| end_match.as_str().parse().unwrap());
.map_or(left, |end_match| end_match.as_str().parse().unwrap());
let name = &cap.get(3).unwrap().as_str();

// Perform sanity checks.
if start > end {
bail!(
"Field must end after it started (expected {} <= {})",
start,
end,
);
// Perform some sanity checks.
let Some((mut start, len)) = if increasing {
right.checked_sub(left)
} else {
left.checked_sub(right)
}
.map(|len| (left, len + 1)) else {
bail!(
"Field must end after it started ({}-{})",
left, right
)};

if let Some(field) = fields.last() {
if field.end <= start {
if !increasing {
// Cancel the massaging to get back what was input
let prev_end = width - field.end();

if prev_end < start {
bail!(
"Field must start after previous ended (expected {} > {})",
prev_end - 1,
start
);
}
} else if field.end() > start {
bail!(
"Field must start after previous ended (expected {} > {})",
field.end,
start,
"Field must start after previous ended (expected {} < {})",
field.end() - 1,
start
);
}
}

fields.push(BitDescrField { start, end, name });
// If in decreasing order, still store positions in increasing order to simplify processing.
if !increasing {
start = width - 1 - start;
}

fields.push(BitDescrField { start, len, name });

// Advance by the match's length, plus any whitespace after it.
row_str = row_str[cap[0].len()..].trim_start();
}

// Check the name type.
let new_name_type = name.is_empty();
if let Some(name_type) = name_type {
if new_name_type != name_type {
return Err(Error::msg("Row names must all be omitted, or none may be"));
}
} else {
name_type = Some(new_name_type);
}

rows.push((name, fields));
}

Ok(BitDescrAttrs { width, rows })
Ok(BitDescrAttrs {
width,
rows,
increasing,
})
}
}

#[derive(Debug)]
struct BitDescrField<'a> {
start: usize,
end: usize,
len: usize,
name: &'a str,
}

impl BitDescrField<'_> {
fn end(&self) -> usize {
self.start + self.len
}
}
30 changes: 13 additions & 17 deletions src/Audio_Registers.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,29 +88,25 @@ Bit 2-0 - Right output volume (0-7)

This register controls CH1's period sweep functionality.

```
Bit 6-4 - Sweep pace
Bit 3 - Sweep increase/decrease
0: Addition (period increases)
1: Subtraction (period decreases)
Bit 2-0 - Sweep slope control (n: 0-7)
```
{{#bits 8 >
"NR10" 6-4:"Pace" 3:"Direction" 2-0:"Individual step";
}}

The <var>sweep pace</var> dictates how often the period gets changed, in units of 128 Hz ticks[^div_apu] (7.8 ms).
The pace is only reloaded after the following sweep iteration, or when (re)triggering the channel.
However, if bits 4–6 are all set to 0, then iterations are instantly disabled, and the pace will be reloaded immediately if it's set to something else.
- **Pace**: This dictates how often sweep "iterations" happen, in units of 128 Hz ticks[^div_apu] (7.8 ms).
Note that the value written to this field is not re-read by the hardware until a sweep iteration completes, or the channel is [(re)triggered](<#Triggering>).

On each sweep iteration, the period in [`NR13`](<#FF13 — NR13: Channel 1 period low \[write-only\]>) and [`NR14`](<#FF14 — NR14: Channel 1 period high & control>) is modified and written back.
That is, unless <var>n</var> (the slope) is 0, in which case iterations do nothing (in this case, subtraction mode should be set, see below).
However, if `0` is written to this field, then iterations are instantly disabled (but see below), and it will be reloaded as soon as it's set to something else.
- **Direction**: `0` = Addition (period increases); `1` = Subtraction (period decreases)
- **Individual step**: On each iteration, the new period <math><msub><mi>L</mi><mrow><mi>t</mi><mo>+</mo><mn>1</mn></mrow></msub></math> is computed from the current one <math><msub><mi>L</mi><mi>t</mi></msub></math> as follows:

On each tick, the new period <math><msub><mi>L</mi><mrow><mi>t</mi><mo>+</mo><mn>1</mn></mrow></msub></math> is computed from the current one <math><msub><mi>L</mi><mi>t</mi></msub></math> as follows:
<math display="block">
<msub><mi>L</mi><mrow><mi>t</mi><mo>+</mo><mn>1</mn></mrow></msub> <mo>=</mo> <msub><mi>L</mi><mi>t</mi></msub> <mo>±</mo> <mfrac><msub><mi>L</mi><mi>t</mi></msub><msup><mn>2</mn><mi>step</mi></msup></mfrac>
</math>

<math display="block">
<msub><mi>L</mi><mrow><mi>t</mi><mo>+</mo><mn>1</mn></mrow></msub> <mo>=</mo> <msub><mi>L</mi><mi>t</mi></msub> <mo>±</mo> <mfrac><msub><mi>L</mi><mi>t</mi></msub><msup><mn>2</mn><mi>n</mi></msup></mfrac>
</math>
On each sweep iteration, the period in [`NR13`](<#FF13 — NR13: Channel 1 period low \[write-only\]>) and [`NR14`](<#FF14 — NR14: Channel 1 period high & control>) is modified and written back.

In addition mode, if the period value would overflow (i.e. <math><msub><mi>L</mi><mrow><mi>t</mi><mo>+</mo><mn>1</mn></mrow></msub></math> is strictly more than $7FF), the channel is turned off instead.
**This occurs even if sweep iterations are disabled** by <var>n</var> = 0.
**This occurs even if sweep iterations are disabled** by the <var>pace</var> being 0.

Note that if the period ever becomes 0, the period sweep will never be able to change it.
For the same reason, the period sweep cannot underflow the period (which would turn the channel off).
Expand Down
Loading

0 comments on commit 23df373

Please sign in to comment.