2018-11-19 16:19:13 +11:00
<!DOCTYPE HTML>
< html lang = "en" class = "sidebar-visible no-js" >
< head >
<!-- Book generated using mdBook -->
< meta charset = "UTF-8" >
2018-11-20 20:57:43 +11:00
< title > GBA Memory Mapping - Rust GBA Guide< / title >
2018-11-19 16:19:13 +11:00
< meta content = "text/html; charset=utf-8" http-equiv = "Content-Type" >
< meta name = "description" content = "" >
< meta name = "viewport" content = "width=device-width, initial-scale=1" >
< meta name = "theme-color" content = "#ffffff" / >
< link rel = "shortcut icon" href = "../favicon.png" >
< link rel = "stylesheet" href = "../css/variables.css" >
< link rel = "stylesheet" href = "../css/general.css" >
< link rel = "stylesheet" href = "../css/chrome.css" >
< link rel = "stylesheet" href = "../css/print.css" media = "print" >
<!-- Fonts -->
< link rel = "stylesheet" href = "../FontAwesome/css/font-awesome.css" >
< link href = "https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800" rel = "stylesheet" type = "text/css" >
< link href = "https://fonts.googleapis.com/css?family=Source+Code+Pro:500" rel = "stylesheet" type = "text/css" >
<!-- Highlight.js Stylesheets -->
< link rel = "stylesheet" href = "../highlight.css" >
< link rel = "stylesheet" href = "../tomorrow-night.css" >
< link rel = "stylesheet" href = "../ayu-highlight.css" >
<!-- Custom theme stylesheets -->
< / head >
< body class = "light" >
<!-- Provide site root to javascript -->
< script type = "text/javascript" > var path _to _root = "../" ; < / script >
<!-- Work around some values being stored in localStorage wrapped in quotes -->
< script type = "text/javascript" >
try {
var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar');
if (theme.startsWith('"') & & theme.endsWith('"')) {
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
}
if (sidebar.startsWith('"') & & sidebar.endsWith('"')) {
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
}
} catch (e) { }
< / script >
<!-- Set the theme before any content is loaded, prevents flash -->
< script type = "text/javascript" >
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = 'light'; }
document.body.className = theme;
document.querySelector('html').className = theme + ' js';
< / script >
<!-- Hide / unhide sidebar before it is displayed -->
< script type = "text/javascript" >
var html = document.querySelector('html');
var sidebar = 'hidden';
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
}
html.classList.remove('sidebar-visible');
html.classList.add("sidebar-" + sidebar);
< / script >
< nav id = "sidebar" class = "sidebar" aria-label = "Table of contents" >
2018-11-20 20:57:43 +11:00
< ol class = "chapter" > < li > < a href = "../introduction.html" > < strong aria-hidden = "true" > 1.< / strong > Introduction< / a > < / li > < li > < a href = "../ch00/index.html" > < strong aria-hidden = "true" > 2.< / strong > Ch 0: Development Setup< / a > < / li > < li > < a href = "../ch01/index.html" > < strong aria-hidden = "true" > 3.< / strong > Ch 1: Hello GBA< / a > < / li > < li > < ol class = "section" > < li > < a href = "../ch01/hello1.html" > < strong aria-hidden = "true" > 3.1.< / strong > hello1< / a > < / li > < li > < a href = "../ch01/volatile.html" > < strong aria-hidden = "true" > 3.2.< / strong > Volatile< / a > < / li > < li > < a href = "../ch01/io_registers.html" > < strong aria-hidden = "true" > 3.3.< / strong > IO Registers< / a > < / li > < li > < a href = "../ch01/the_display_control_register.html" > < strong aria-hidden = "true" > 3.4.< / strong > The Display Control Register< / a > < / li > < li > < a href = "../ch01/video_memory_intro.html" > < strong aria-hidden = "true" > 3.5.< / strong > Video Memory Intro< / a > < / li > < li > < a href = "../ch01/hello2.html" > < strong aria-hidden = "true" > 3.6.< / strong > hello2< / a > < / li > < / ol > < / li > < li > < a href = "../ch02/index.html" > < strong aria-hidden = "true" > 4.< / strong > Ch 2: User Input< / a > < / li > < li > < ol class = "section" > < li > < a href = "../ch02/the_key_input_register.html" > < strong aria-hidden = "true" > 4.1.< / strong > The Key Input Register< / a > < / li > < li > < a href = "../ch02/the_vcount_register.html" > < strong aria-hidden = "true" > 4.2.< / strong > The VCount Register< / a > < / li > < li > < a href = "../ch02/light_cycle.html" > < strong aria-hidden = "true" > 4.3.< / strong > light_cycle< / a > < / li > < / ol > < / li > < li > < a href = "../ch03/index.html" > < strong aria-hidden = "true" > 5.< / strong > Ch 3: Memory and Objects< / a > < / li > < li > < ol class = "section" > < li > < a href = "../ch03/gba_memory_mapping.html" class = "active" > < strong aria-hidden = "true" > 5.1.< / strong > GBA Memory Mapping< / a > < / li > < li > < a href = "../ch03/tile_data.html" > < strong aria-hidden = "true" > 5.2.< / strong > Tile Data< / a > < / li > < li > < a href = "../ch03/regular_backgrounds.html" > < strong aria-hidden = "true" > 5.3.< / strong > Regular Backgrounds< / a > < / li > < li > < a href = "../ch03/object_basics.html" > < strong aria-hidden = "true" > 5.4.< / strong > Object Basics< / a > < / li > < li > < a href = "../ch03/gba_rng.html" > < strong aria-hidden = "true" > 5.5.< / strong > GBA RNG< / a > < / li > < li > < a href = "../ch03/memory_game.html" > < strong aria-hidden = "true" > 5.6.< / strong > memory_game< / a > < / li > < / ol > < / li > < / ol >
2018-11-19 16:19:13 +11:00
< / nav >
< div id = "page-wrapper" class = "page-wrapper" >
< div class = "page" >
< div id = "menu-bar" class = "menu-bar" >
< div id = "menu-bar-sticky-container" >
< div class = "left-buttons" >
< button id = "sidebar-toggle" class = "icon-button" type = "button" title = "Toggle Table of Contents" aria-label = "Toggle Table of Contents" aria-controls = "sidebar" >
< i class = "fa fa-bars" > < / i >
< / button >
< button id = "theme-toggle" class = "icon-button" type = "button" title = "Change theme" aria-label = "Change theme" aria-haspopup = "true" aria-expanded = "false" aria-controls = "theme-list" >
< i class = "fa fa-paint-brush" > < / i >
< / button >
< ul id = "theme-list" class = "theme-popup" aria-label = "Themes" role = "menu" >
< li role = "none" > < button role = "menuitem" class = "theme" id = "light" > Light < span class = "default" > (default)< / span > < / button > < / li >
< li role = "none" > < button role = "menuitem" class = "theme" id = "rust" > Rust< / button > < / li >
< li role = "none" > < button role = "menuitem" class = "theme" id = "coal" > Coal< / button > < / li >
< li role = "none" > < button role = "menuitem" class = "theme" id = "navy" > Navy< / button > < / li >
< li role = "none" > < button role = "menuitem" class = "theme" id = "ayu" > Ayu< / button > < / li >
< / ul >
< button id = "search-toggle" class = "icon-button" type = "button" title = "Search. (Shortkey: s)" aria-label = "Toggle Searchbar" aria-expanded = "false" aria-keyshortcuts = "S" aria-controls = "searchbar" >
< i class = "fa fa-search" > < / i >
< / button >
< / div >
< h1 class = "menu-title" > Rust GBA Guide< / h1 >
< div class = "right-buttons" >
< a href = "../print.html" title = "Print this book" aria-label = "Print this book" >
< i id = "print-button" class = "fa fa-print" > < / i >
< / a >
< / div >
< / div >
< / div >
< div id = "search-wrapper" class = "hidden" >
< form id = "searchbar-outer" class = "searchbar-outer" >
< input type = "search" name = "search" id = "searchbar" name = "searchbar" placeholder = "Search this book ..." aria-controls = "searchresults-outer" aria-describedby = "searchresults-header" >
< / form >
< div id = "searchresults-outer" class = "searchresults-outer hidden" >
< div id = "searchresults-header" class = "searchresults-header" > < / div >
< ul id = "searchresults" >
< / ul >
< / div >
< / div >
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
< script type = "text/javascript" >
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
});
< / script >
< div id = "content" class = "content" >
< main >
2018-11-20 20:57:43 +11:00
< a class = "header" href = "#gba-memory-mapping" id = "gba-memory-mapping" > < h1 > GBA Memory Mapping< / h1 > < / a >
2018-11-19 16:19:13 +11:00
< p > The < a href = "http://problemkaputt.de/gbatek.htm#gbamemorymap" > GBA Memory Map< / a > has
several memory portions to it, each with their own little differences. Most of
the memory has pre-determined use according to the hardware, but there is also
space for games to use as a scratch pad in whatever way the game sees fit.< / p >
2018-11-21 18:18:34 +11:00
< p > The memory ranges listed here are < em > inclusive< / em > , so they end with a lot of F's
and E's.< / p >
2018-11-19 16:19:13 +11:00
< p > We've talked about volatile memory before, but just as a reminder I'll say that
2018-11-21 18:18:34 +11:00
all of the memory we'll talk about here should be accessed using volatile with
2018-11-19 16:19:13 +11:00
two exceptions:< / p >
< ol >
< li > Work RAM (both internal and external) can be used normally, and if the
2018-11-21 18:18:34 +11:00
compiler is able to totally elide some reads and writes that's okay.< / li >
2018-11-19 16:19:13 +11:00
< li > However, if you set aside any space in Work RAM where an interrupt will
communicate with the main program then that specific location will have to
keep using volatile access, since the compiler never knows when an interrupt
will actually happen.< / li >
< / ol >
< a class = "header" href = "#bios--system-rom" id = "bios--system-rom" > < h2 > BIOS / System ROM< / h2 > < / a >
< ul >
< li > < code > 0x0< / code > to < code > 0x3FFF< / code > (16k)< / li >
< / ul >
< p > This is special memory for the BIOS. It is " read-only" , but even then it's only
accessible when the program counter is pointing into the BIOS region. At all
other times you get a < a href = "http://problemkaputt.de/gbatek.htm#gbaunpredictablethings" > garbage
value< / a > back when you
try to read out of the BIOS.< / p >
< a class = "header" href = "#external-work-ram--ewram" id = "external-work-ram--ewram" > < h2 > External Work RAM / EWRAM< / h2 > < / a >
< ul >
< li > < code > 0x2000000< / code > to < code > 0x203FFFF< / code > (256k)< / li >
< / ul >
< p > This is a big pile of space, the use of which is up to each game. However, the
external work ram has only a 16-bit bus (if you read/write a 32-bit value it
silently breaks it up into two 16-bit operations) and also 2 wait cycles (extra
CPU cycles that you have to expend < em > per 16-bit bus use< / em > ).< / p >
2018-11-21 18:18:34 +11:00
< p > It's most helpful to think of EWRAM as slower, distant memory, similar to the
" heap" in a normal application. You can take the time to go store something
within EWRAM, or to load it out of EWRAM, but if you've got several operations
to do in a row and you're worried about time you should pull that value into
local memory, work on your local copy, and then push it back out to EWRAM.< / p >
2018-11-19 16:19:13 +11:00
< a class = "header" href = "#internal-work-ram--iwram" id = "internal-work-ram--iwram" > < h2 > Internal Work RAM / IWRAM< / h2 > < / a >
< ul >
< li > < code > 0x3000000< / code > to < code > 0x3007FFF< / code > (32k)< / li >
< / ul >
< p > This is a smaller pile of space, but it has a 32-bit bus and no wait.< / p >
< p > By default, < code > 0x3007F00< / code > to < code > 0x3007FFF< / code > is reserved for interrupt and BIOS use.
The rest of it is totally up to you. The user's stack space starts at
2018-11-21 18:18:34 +11:00
< code > 0x3007F00< / code > and proceeds < em > down< / em > from there. For best results you should probably
start at < code > 0x3000000< / code > and then go upwards. Under normal use it's unlikely that
the two memory regions will crash into each other.< / p >
2018-11-19 16:19:13 +11:00
< a class = "header" href = "#io-registers" id = "io-registers" > < h2 > IO Registers< / h2 > < / a >
< ul >
< li > < code > 0x4000000< / code > to < code > 0x40003FE< / code > < / li >
< / ul >
< p > We've touched upon a few of these so far, and we'll get to more later. At the
moment it is enough to say that, as you might have guessed, all of them live in
this region. Each individual register is a < code > u16< / code > or < code > u32< / code > and they control all
sorts of things. We'll actually be talking about some more of them in this very
chapter, because that's how we'll control some of the background and object
stuff.< / p >
< a class = "header" href = "#palette-ram--palram" id = "palette-ram--palram" > < h2 > Palette RAM / PALRAM< / h2 > < / a >
< ul >
< li > < code > 0x5000000< / code > to < code > 0x50003FF< / code > (1k)< / li >
< / ul >
< p > Palette RAM has a 16-bit bus, which isn't really a problem because it
conceptually just holds < code > u16< / code > values. There's no automatic wait state, but if
you try to access the same location that the display controller is accessing you
get bumped by 1 cycle. Since the display controller can use the palette ram any
number of times per scanline it's basically impossible to predict if you'll have
to do a wait or not during VDraw. During VBlank you won't have any wait of
course.< / p >
< p > PALRAM is among the memory where there's weirdness if you try to write just one
byte: if you try to write just 1 byte, it writes that byte into < em > both< / em > parts of
the larger 16-bit location. This doesn't really affect us much with PALRAM,
because palette values are all supposed to be < code > u16< / code > anyway.< / p >
< p > The palette memory actually contains not one, but < em > two< / em > sets of palettes. First
there's 256 entries for the background palette data (starting at < code > 0x5000000< / code > ),
and then there's 256 entries for object palette data (starting at < code > 0x5000200< / code > ).< / p >
< p > The GBA also has two modes for palette access: 8-bits-per-pixel (8bpp) and
4-bits-per-pixel (4bpp).< / p >
< ul >
2018-11-21 18:18:34 +11:00
< li > In 8bpp mode an 8-bit palette index value within a background or sprite
2018-11-19 16:19:13 +11:00
simply indexes directly into the 256 slots for that type of thing.< / li >
2018-11-21 18:18:34 +11:00
< li > In 4bpp mode a 4-bit palette index value within a background or sprite
2018-11-19 16:19:13 +11:00
specifies an index within a particular " palbank" (16 palette entries each),
and then a < em > separate< / em > setting outside of the graphical data determines which
palbank is to be used for that background or object (the screen entry data for
backgrounds, and the object attributes for objects).< / li >
< / ul >
< a class = "header" href = "#video-ram--vram" id = "video-ram--vram" > < h2 > Video RAM / VRAM< / h2 > < / a >
< ul >
< li > < code > 0x6000000< / code > to < code > 0x6017FFF< / code > (96k)< / li >
< / ul >
< p > We've used this before! VRAM has a 16-bit bus and no wait. However, the same as
with PALRAM, the " you might have to wait if the display controller is looking at
it" rule applies here.< / p >
< p > Unfortunately there's not much more exact detail that can be given about VRAM.
The use of the memory depends on the video mode that you're using.< / p >
< p > One general detail of note is that you can't write individual bytes to any part
of VRAM. Depending on mode and location, you'll either get your bytes doubled
into both the upper and lower parts of the 16-bit location targeted, or you
won't even affect the memory. This usually isn't a big deal, except in two
situations:< / p >
< ul >
< li > In Mode 4, if you want to change just 1 pixel, you'll have to be very careful
to read the old < code > u16< / code > , overwrite just the byte you wanted to change, and then
write that back.< / li >
< li > In any display mode, avoid using < code > memcopy< / code > to place things into VRAM.
It's written to be byte oriented, and only does 32-bit transfers under select
conditions. The rest of the time it'll copy one byte at a time and you'll get
either garbage or nothing at all.< / li >
< / ul >
< a class = "header" href = "#object-attribute-memory--oam" id = "object-attribute-memory--oam" > < h2 > Object Attribute Memory / OAM< / h2 > < / a >
< ul >
< li > < code > 0x7000000< / code > to < code > 0x70003FF< / code > (1k)< / li >
< / ul >
< p > The Object Attribute Memory has a 32-bit bus and no default wait, but suffers
from the " you might have to wait if the display controller is looking at it"
rule. You cannot write individual bytes to OAM at all, but that's not really a
problem because all the fields of the data types within OAM are either < code > i16< / code > or
< code > u16< / code > anyway.< / p >
< p > Object attribute memory is the wildest yet: it conceptually contains two types
of things, but they're < em > interlaced< / em > with each other all the way through.< / p >
< p > Now, < a href = "http://problemkaputt.de/gbatek.htm#lcdobjoamattributes" > GBATEK< / a > and
< a href = "https://www.cs.rit.edu/%7Etjh8300/CowBite/CowBiteSpec.htm#OAM%20(sprites)" > CowByte< / a >
2018-11-21 18:18:34 +11:00
doesn't quite give names to the two data types here.
2018-11-19 16:19:13 +11:00
< a href = "https://www.coranac.com/tonc/text/regobj.htm#sec-oam" > TONC< / a > calls them
2018-11-21 18:18:34 +11:00
< code > OBJ_ATTR< / code > and < code > OBJ_AFFINE< / code > , but we'll be giving them names fitting with the
Rust naming convention. Just know that if you try to talk about it with others
they might not be using the same names. In Rust terms their layout would look
like this:< / p >
2018-11-19 16:19:13 +11:00
< pre > < pre class = "playpen" > < code class = "language-rust" >
# #![allow(unused_variables)]
#fn main() {
#[repr(C)]
2018-11-21 18:18:34 +11:00
pub struct ObjectAttributes {
2018-11-19 16:19:13 +11:00
attr0: u16,
attr1: u16,
attr2: u16,
filler: i16,
}
#[repr(C)]
pub struct AffineMatrix {
filler0: [u16; 3],
pa: i16,
filler1: [u16; 3],
pb: i16,
filler2: [u16; 3],
pc: i16,
filler3: [u16; 3],
pd: i16,
}
#}< / code > < / pre > < / pre >
< p > (Note: the < code > #[repr(C)]< / code > part just means that Rust must lay out the data exactly
in the order we specify, which otherwise it is not required to do).< / p >
2018-11-21 18:18:34 +11:00
< p > So, we've got 1024 bytes in OAM and each < code > ObjectAttributes< / code > value is 8 bytes, so
2018-11-19 16:19:13 +11:00
naturally we can support up to 128 objects.< / p >
< p > < em > At the same time< / em > , we've got 1024 bytes in OAM and each < code > AffineMatrix< / code > is 32
bytes, so we can have 32 of them.< / p >
< p > But, as I said, these things are all < em > interlaced< / em > with each other. See how
there's " filler" fields in each struct? If we imagine the OAM as being just an
2018-11-21 18:18:34 +11:00
array of one type or the other, indexes 0/1/2/3 of the < code > ObjectAttributes< / code > array
2018-11-19 16:19:13 +11:00
would line up with index 0 of the < code > AffineMatrix< / code > array. It's kinda weird, but
that's just how it works. When we setup functions to read and write these values
we'll have to be careful with how we do it. We probably < em > won't< / em > want to use
those representations above, at least not with the < code > AffineMatrix< / code > type, because
they're quite wasteful if you want to store just object attributes or just
affine matrices.< / p >
< a class = "header" href = "#game-pak-rom--flash-rom" id = "game-pak-rom--flash-rom" > < h2 > Game Pak ROM / Flash ROM< / h2 > < / a >
< ul >
< li > < code > 0x8000000< / code > to < code > 0x9FFFFFF< / code > (wait 0)< / li >
< li > < code > 0xA000000< / code > to < code > 0xBFFFFFF< / code > (wait 1)< / li >
< li > < code > 0xC000000< / code > to < code > 0xDFFFFFF< / code > (wait 2)< / li >
< li > Max of 32Mb< / li >
< / ul >
< p > These portions of the memory are less fixed, because they depend on the precise
details of the game pak you've inserted into the GBA. In general, they connect
to the game pak ROM and/or Flash memory, using a 16-bit bus. The ROM is
read-only, but the Flash memory (if any) allows writes.< / p >
< p > The game pak ROM is listed as being in three sections, but it's actually the
same memory being effectively mirrored into three different locations. The
mirror that you choose to access the game pak through affects which wait state
setting it uses (configured via IO register of course). Unfortunately, the
details come down more to the game pak hardware that you load your game onto
than anything else, so there's not much I can say right here. We'll eventually
2018-11-21 18:18:34 +11:00
talk about it more later when I'm forced to do the boring thing and just cover
all the IO registers that aren't covered anywhere else.< / p >
2018-11-19 16:19:13 +11:00
< p > One thing of note is the way that the 16-bit bus affects us: the instructions to
execute are coming through the same bus as the rest of the game data, so we want
them to be as compact as possible. The ARM chip in the GBA supports two
different instruction sets, " thumb" and " non-thumb" . The thumb mode instructions
are 16-bit, so they can each be loaded one at a time, and the non-thumb
instructions are 32-bit, so we're at a penalty if we execute them directly out
of the game pak. However, some things will demand that we use non-thumb code, so
we'll have to deal with that eventually. It's possible to switch between modes,
but it's a pain to keep track of what mode you're in because there's not
currently support for it in Rust itself (perhaps some day). So we'll stick with
thumb code as much as we possibly can, that's why our target profile for our
builds starts with < code > thumbv4< / code > .< / p >
< a class = "header" href = "#game-pak-sram" id = "game-pak-sram" > < h2 > Game Pak SRAM< / h2 > < / a >
< ul >
< li > < code > 0xE000000< / code > to < code > 0xE00FFFF< / code > (64k)< / li >
< / ul >
2018-11-19 20:25:45 +11:00
< p > The game pak SRAM has an 8-bit bus. Why did Pokémon always take so long to save?
2018-11-21 18:18:34 +11:00
Saving the whole game one byte at a time is why. The SRAM also has some amount
of wait, but as with the ROM, the details depend on your game pak hardware (and
also as with ROM, you can adjust the settings with an IO register, should you
need to).< / p >
2018-11-19 16:19:13 +11:00
< p > One thing to note about the SRAM is that the GBA has a Direct Memory Access
(DMA) feature that can be used for bulk memory movements in some cases, but the
DMA < em > cannot< / em > access the SRAM region. You really are stuck reading and writing
one byte at a time when you're using the SRAM.< / p >
< / main >
< nav class = "nav-wrapper" aria-label = "Page navigation" >
<!-- Mobile navigation buttons -->
< a rel = "prev" href = "../ch03/index.html" class = "mobile-nav-chapters previous" title = "Previous chapter" aria-label = "Previous chapter" aria-keyshortcuts = "Left" >
< i class = "fa fa-angle-left" > < / i >
< / a >
2018-11-20 20:57:43 +11:00
< a rel = "next" href = "../ch03/tile_data.html" class = "mobile-nav-chapters next" title = "Next chapter" aria-label = "Next chapter" aria-keyshortcuts = "Right" >
2018-11-19 16:19:13 +11:00
< i class = "fa fa-angle-right" > < / i >
< / a >
< div style = "clear: both" > < / div >
< / nav >
< / div >
< / div >
< nav class = "nav-wide-wrapper" aria-label = "Page navigation" >
< a href = "../ch03/index.html" class = "nav-chapters previous" title = "Previous chapter" aria-label = "Previous chapter" aria-keyshortcuts = "Left" >
< i class = "fa fa-angle-left" > < / i >
< / a >
2018-11-20 20:57:43 +11:00
< a href = "../ch03/tile_data.html" class = "nav-chapters next" title = "Next chapter" aria-label = "Next chapter" aria-keyshortcuts = "Right" >
2018-11-19 16:19:13 +11:00
< i class = "fa fa-angle-right" > < / i >
< / a >
< / nav >
< / div >
< script src = "../elasticlunr.min.js" type = "text/javascript" charset = "utf-8" > < / script >
< script src = "../mark.min.js" type = "text/javascript" charset = "utf-8" > < / script >
< script src = "../searcher.js" type = "text/javascript" charset = "utf-8" > < / script >
< script src = "../clipboard.min.js" type = "text/javascript" charset = "utf-8" > < / script >
< script src = "../highlight.js" type = "text/javascript" charset = "utf-8" > < / script >
< script src = "../book.js" type = "text/javascript" charset = "utf-8" > < / script >
<!-- Custom JS scripts -->
< / body >
< / html >