Browse Source

fxsdk: initial push for gint v2 (WIP but mostly done)

pull/4/head
Lephe 1 year ago
commit
d19025bbc9
18 changed files with 2287 additions and 0 deletions
  1. +17
    -0
      .gitignore
  2. +119
    -0
      Makefile
  3. +112
    -0
      configure
  4. +112
    -0
      fxconv/fxconv-main.py
  5. +497
    -0
      fxconv/fxconv.py
  6. +146
    -0
      fxg1a/dump.c
  7. +71
    -0
      fxg1a/edit.c
  8. +111
    -0
      fxg1a/file.c
  9. +168
    -0
      fxg1a/fxg1a.h
  10. +59
    -0
      fxg1a/g1a.h
  11. +132
    -0
      fxg1a/icon.c
  12. +266
    -0
      fxg1a/main.c
  13. +47
    -0
      fxg1a/util.c
  14. +154
    -0
      fxos/fxos.h
  15. +183
    -0
      fxos/main.c
  16. +59
    -0
      fxos/tables.c
  17. +30
    -0
      fxos/util.c
  18. +4
    -0
      fxsdk/main.c

+ 17
- 0
.gitignore View File

@ -0,0 +1,17 @@
# Configuration file
Makefile.cfg
# Build directory
build/
# Binaries
bin/
# Test icons
icons/
# Documentation drafts
doc/
# Python cache
__pycache__

+ 119
- 0
Makefile View File

@ -0,0 +1,119 @@
#! /usr/bin/make -f
# Require config file if not cleaning up
ifeq "$(filter clean distclean,$(MAKECMDGOALS))" ""
include Makefile.cfg
endif
# Compiler flags
cflags = -Wall -Wextra -std=c11 -O2 -I $(dir $<) -D_GNU_SOURCE \
-DFXSDK_PREFIX='"$(PREFIX)"' $(CFLAGS)
# Linker flags
lflags = -lpng
# Bison generation flags
# bflags = -L C --defines=$(@:.c=.h) --verbose
# Dependency generation flags
dflags = -MT $@ -MMD -MP -MF $(@:%.o=%.d)
#
# Main targets and symbolic targets
# $TARGETS is provided by Makefile.cfg.
#
TARGETS := $(filter-out fxconv,$(TARGETS))
bin = $(TARGETS:%=bin/%)
# fxconv has no sources files because it's written in Python
src = $(wildcard $1/*.c)
src-fxsdk := $(call src,fxsdk)
src-fxg1a := $(call src,fxg1a)
src-fxos := $(call src,fxos)
obj = $(src-$1:%=build/%.o)
obj-fxsdk := $(call obj,fxsdk)
obj-fxg1a := $(call obj,fxg1a)
obj-fxos := $(call obj,fxos)
# Symbolic targets
all: $(bin)
all-fxsdk: bin/fxsdk
all-fxg1a: bin/fxg1a
all-fxos: bin/fxos
# Explicit targets
bin/fxsdk: $(obj-fxsdk) | bin/
gcc $^ -o $@ $(lflags)
bin/fxg1a: $(obj-fxg1a) | bin/
gcc $^ -o $@ $(lflags)
bin/fxos: $(obj-fxos) | bin/
gcc $^ -o $@ $(lflags)
bin/:
mkdir -p $@
#
# Source rules
#
build/%.c.o: %.c
@mkdir -p $(dir $@)
gcc -c $< -o $@ $(cflags) $(dflags)
# Flex lexers (unused since fxconv is written in Python)
# build/%/lexer.yy.c: %/lexer.l build/%/parser.tab.c
# flex -o $@ -s $<
# build/%/lexer.yy.c.o: build/%/lexer.yy.c
# gcc -c $< -o $@ $(cflags) -Wno-unused-function $(dflags) -I $*
# Bison parsers (unused since fxconv is written in Python)
# build/%/parser.tab.c: %/parser.y
# bison $< -o $@ $(bflags)
# build/%/parser.tab.c.o: build/%/parser.tab.c
# gcc -c $< -o $@ $(cflags) $(dflags) -I $*
#
# Dependency system, misc.
#
include $(wildcard build/*/*.d)
# Dependency on configuration file
Makefile.cfg:
@ if [[ ! -f Makefile.cfg ]]; then \
echo "error: Makefile.cfg is missing, did you ./configure?" >&2; \
false; \
fi
.PHONY: all clean distclean
#
# Installing
#
install: $(bin)
install -d $(PREFIX)/bin
install $(bin) -m 755 $(PREFIX)/bin
install fxconv/fxconv-main.py -m 755 $(PREFIX)/bin/fxconv
install fxconv/fxconv.py -m 644 $(PREFIX)/bin
#
# Cleaning
#
clean-fxsdk:
@rm -rf build/fxsdk
clean-fxconv:
@rm -rf build/fxconv
clean-fxg1a:
@rm -rf build/fxg1a
clean-fxos:
@rm -rf build/fxos
clean:
@rm -rf build
distclean: clean
@rm -rf bin
@rm -f Makefile.cfg

+ 112
- 0
configure View File

@ -0,0 +1,112 @@
#! /usr/bin/bash
#
# Output variables
#
# Path parameters
PREFIX="/usr"
# Individual component selection
BUILD_fxsdk=1
BUILD_fxconv=1
BUILD_fxg1a=1
BUILD_fxos=1
#
# Tool name checking
#
check()
{
[[ $1 = "fxsdk" ]] ||
[[ $1 = "fxconv" ]] ||
[[ $1 = "fxg1a" ]] ||
[[ $1 = "fxos" ]]
}
#
# Usage
#
help()
{
cat << EOF
Configuration options for the fxSDK (fx9860g and fxcg50 development tools).
Tool selection:
<tool> may be one of the following:
"fxsdk" Command-line options (you generally want this)
"fxconv" Asset conversion for gint (or any 4-aligned-VRAM system)
"fxg1a" G1A file wrapper, editor and analyzer
"fxos" OS fiddling tool, including syscall disassembly
--enable-<tool> Build and install the selected tool [default]
--disable-<tool> Do not build or install the selected tool
Install folders:
Executables will be installed in <prefix>/bin and runtime data in
<prefix>/share/fxsdk.
--prefix=<prefix> Base install folder [default /usr]
EOF
exit 0
}
#
# Argument parsing
#
for arg; do case "$arg" in
-h | -? | --help)
help;;
--prefix=*)
PREFIX=${arg#--prefix=};;
--enable-*)
tool="${arg#--enable-}"
if ! check $tool; then
echo "error: cannot enable $tool: unknown tool"
exit 1
fi
eval "BUILD_${tool}=1";;
--disable-*)
tool="${arg#--disable-}"
if ! check $tool; then
echo "error: cannot disable $tool: unknown tool"
exit 1
fi
eval "BUILD_${tool}=0";;
*)
echo "error: unrecognized option $arg"
exit 1;;
esac; done
#
# Makefile generation
#
gen()
{
echo "PREFIX = $PREFIX"
echo -n "TARGETS ="
[[ $BUILD_fxsdk = 1 ]] && echo -n " fxsdk"
[[ $BUILD_fxconv = 1 ]] && echo -n " fxconv"
[[ $BUILD_fxg1a = 1 ]] && echo -n " fxg1a"
[[ $BUILD_fxos = 1 ]] && echo -n " fxos"
echo ""
}
echo "Configuration complete, the following has been saved in Makefile.cfg:"
echo ""
gen | tee Makefile.cfg
echo ""
echo "You can now 'make'."

+ 112
- 0
fxconv/fxconv-main.py View File

@ -0,0 +1,112 @@
#! /usr/bin/python3
import getopt
import sys
import os
import fxconv
help_string = """
usage: fxconv [-s] <python script> [files...]
fxconv -b <bin file> -o <object file> [parameters...]
fxconv -i <png file> -o <object file> [parameters...]
fxconv -f <png file> -o <object file> [parameters...]
fxconv converts data files such as images and fonts into gint formats
optimized for fast execution, or into object files.
Operating modes:
-s, --script Expose the fxconv module and run this Python script
-b, --binary Turn data into an object file, no conversion
-i, --image Convert to gint's image format
-f, --font Convert to gint's font format
When using -s, additional arguments are stored in the [fxconv.args] variable of
the module. This is intended to be a restricted list of file names specified by
a Makefile, used to convert only a subset of the files in the script.
The -b, -i and -f modes are shortcuts to convert single files without a script.
They accept parameters with a "category.key:value" syntax, for example:
fxconv -f myfont.png -o myfont.o charset:ascii grid.padding:1 height:7
""".strip()
# Simple error-warnings system
def err(msg):
print("error:", msg, file=sys.stderr)
def warn(msg):
print("warning:", msg, file=sys.stderr)
def main():
# Default execution mode is to run a Python script for conversion
modes = "script binary image font"
mode = "s"
output = None
# Parse command-line arguments
if len(sys.argv) == 1:
print(help_string, file=sys.stderr)
sys.exit(1)
try:
opts, args = getopt.gnu_getopt(sys.argv[1:], "hsbifo:",
("help output="+modes).split())
except getopt.GetoptError as error:
err(error)
sys.exit(1)
for name, value in opts:
# Print usage
if name == "--help":
err(help_string, file=sys.stderr)
sys.exit(0)
# TODO: fxconv: verbose mode
elif name == "--verbose":
pass
elif name in [ "-o", "--output" ]:
output = value
# Other names are modes
else:
mode = name[1] if len(name)==2 else name[2]
# Remaining arguments
if args == []:
err(f"execution mode -{mode} expects an input file")
sys.exit(1)
input = args.pop(0)
# In --script mode, run the Python script with an augmented PYTHONPATH
if mode == "s":
if output is not None:
warn("option --output is ignored in script mode")
args = None if args == [] else args
err("script mode not currently implemented (TODO) x_x")
sys.exit(1)
# In shortcut conversion modes, read parameters from the command-line
else:
def check(arg):
if ':' not in arg:
warn(f"argument {arg} is not a valid parameter (ignored)")
return ':' in arg
def insert(params, path, value):
if len(path) == 1:
params[path[0]] = value
return
if not path[0] in params:
params[path[0]] = {}
insert(params[path[0]], path[1:], value)
args = [ arg.split(':', 1) for arg in args if check(arg) ]
params = {}
for (name, value) in args:
insert(params, name.split("."), value)
params["type"] = { "b": "binary", "i": "image", "f": "font" }[mode]
fxconv.convert(input, params, output)
main()

+ 497
- 0
fxconv/fxconv.py View File

@ -0,0 +1,497 @@
"""
fxconv: Convert data files into gint formats or object files
"""
import os
import tempfile
import subprocess
from PIL import Image
#
# Color quantification
#
# Colors
FX_BLACK = ( 0, 0, 0, 255)
FX_DARK = ( 85, 85, 85, 255)
FX_LIGHT = (170, 170, 170, 255)
FX_WHITE = (255, 255, 255, 255)
FX_ALPHA = ( 0, 0, 0, 0)
# Profiles
FX_PROFILES = [
{ # Usual black-and-white bitmaps without transparency, as in MonochromeLib
"name": "mono",
"gray": False,
"colors": { FX_BLACK, FX_WHITE },
"layers": [ lambda c: (c == FX_BLACK) ]
},
{ # Black-and-white with transparency, equivalent of two bitmaps in ML
"name": "mono_alpha",
"gray": False,
"colors": { FX_BLACK, FX_WHITE, FX_ALPHA },
"layers": [ lambda c: (c != FX_ALPHA),
lambda c: (c == FX_BLACK) ]
},
{ # Gray engine bitmaps, reference could have been Eiyeron's Gray Lib
"name": "gray",
"gray": True,
"colors": { FX_BLACK, FX_DARK, FX_LIGHT, FX_WHITE },
"layers": [ lambda c: (c in [FX_BLACK, FX_LIGHT]),
lambda c: (c in [FX_BLACK, FX_DARK]) ]
},
{ # Gray images with transparency, unfortunately 3 layers since 5 colors
"name": "gray_alpha",
"gray": True,
"colors": { FX_BLACK, FX_DARK, FX_LIGHT, FX_WHITE, FX_ALPHA },
"layers": [ lambda c: (c != FX_ALPHA),
lambda c: (c in [FX_BLACK, FX_LIGHT]),
lambda c: (c in [FX_BLACK, FX_DARK]) ]
},
]
#
# Character sets
#
class _Charset:
def __init__(self, id, name, count):
self.id = id
self.name = name
self.count = count
FX_CHARSETS = [
# Digits 0...9
_Charset(0x0, "numeric", 10),
# Uppercase letters A...Z
_Charset(0x1, "upper", 26),
# Upper and lowercase letters A..Z, a..z
_Charset(0x2, "alpha", 52),
# Letters and digits A..Z, a..z, 0..9
_Charset(0x3, "alnum", 62),
# All printable characters from 0x20 to 0x7e
_Charset(0x4, "print", 95),
# All 128 ASII characters
_Charset(0x5, "ascii", 128),
]
#
# Internal routines
#
# normalize_area(): Expand area.size and set defaults for all values.
def _normalize_area(area, img):
default = { "x": 0, "y": 0, "width": img.width, "height": img.height }
if area is None:
area = default
else:
if "size" in area:
area["width"], area["height"] = area["size"].split("x")
area = { **default, **area }
return (int(area[key]) for key in "x y width height".split())
class _Grid:
# [grid] is a dictionary of parameters. Relevant keys:
# "border", "padding", "width", "height", "size"
def __init__(self, grid):
self.border = int(grid.get("border", 1))
self.padding = int(grid.get("padding", 0))
self.w = int(grid.get("width", "-1"))
self.h = int(grid.get("height", "-1"))
if "size" in grid:
self.w, self.h = map(int, grid["size"].split("x"))
if self.w <= 0 or self.h <= 0:
raise FxconvError("size of grid unspecified or invalid")
# size(): Number of elements in the grid
def size(self, img):
b, p, w, h = self.border, self.padding, self.w, self.h
# Padding-extended parameters
W = w + 2 * p
H = h + 2 * p
columns = (img.width - b) // (W + b)
rows = (img.height - b) // (H + b)
return columns * rows
# iter(): Iterator on all rectangles of the grid
def iter(self, img):
b, p, w, h = self.border, self.padding, self.w, self.h
# Padding-extended parameters
W = w + 2 * p
H = h + 2 * p
columns = (img.width - b) // (W + b)
rows = (img.height - b) // (H + b)
for r in range(rows):
for c in range(columns):
x = b + c * (W + b) + p
y = b + r * (H + b) + p
yield (x, y, x + w, y + h)
#
# Binary conversion
#
def _convert_binary(input, output, params):
raise FxconvError("TODO: binary mode x_x")
#
# Image conversion
#
def _profile_find(name):
gen = ((i,pr) for (i,pr) in enumerate(FX_PROFILES) if pr["name"] == name)
return next(gen, (None,None))
def _convert_image(input, output, params):
img = Image.open(input)
if img.width >= 4096 or img.height >= 4096:
raise FxconvError(f"'{input}' is too large (max. 4095*4095)")
# Expand area.size and get the defaults. Crop image to resulting area.
params["area"] = _normalize_area(params.get("area", None), img)
img = img.crop(params["area"])
# Quantize the image and check the profile
img = quantize(img, dither=False)
# If profile is provided, check its validity, otherwise use the smallest
# compatible profile
colors = { y for (x,y) in img.getcolors() }
if "profile" in params:
p = params["profile"]
pid, p = _profile_find(p)
if p is None:
raise FxconvError(f"unknown profile {p} in conversion '{input}'")
if colors - profiles[p]:
raise FxconvError(f"'{input}' has more colors than profile '{p}'")
else:
p = "gray" if FX_LIGHT in colors or FX_DARK in colors else "mono"
if FX_ALPHA in colors: p += "_alpha"
pid, p = _profile_find(p)
# Make the image header
header = bytes ([(0x80 if p["gray"] else 0) + pid])
encode24bit = lambda x: bytes([ x >> 16, (x & 0xff00) >> 8, x & 0xff ])
header += encode24bit((img.size[0] << 12) + img.size[1])
# Split the image into layers depending on the profile and zip them all
layers = [ _image_project(img, layer) for layer in p["layers"] ]
count = len(layers)
size = len(layers[0])
data = bytearray(count * size)
n = 0
for longword in range(size // 4):
for layer in layers:
for i in range(4):
data[n] = layer[4 * longword + i]
n += 1
# Generate the object file
elf(header + data, output, "_" + params["name"])
def _image_project(img, f):
# New width and height
w = (img.size[0] + 31) // 32
h = (img.size[1])
data = bytearray(4 * w * h)
im = img.load()
# Now generate a 32-bit byte sequence
for y in range(img.size[1]):
for x in range(img.size[0]):
bit = int(f(im[x, y]))
data[4 * y * w + (x >> 3)] |= (bit << (~x & 7))
return data
#
# Font conversion
#
def _charset_find(name):
gen = (cs for cs in FX_CHARSETS if cs.name == name)
return next(gen, None)
def _convert_font(input, output, params):
#--
# Image area and grid
#--
img = Image.open(input)
params["area"] = _normalize_area(params.get("area", None), img)
img = img.crop(params["area"])
grid = _Grid(params.get("grid", {}))
# Quantize image (any profile will do)
img = quantize(img, dither=False)
#--
# Character set
#--
if "charset" not in params:
raise FxconvError("'charset' attribute is required and missing")
charset = _charset_find(params["charset"])
if charset is None:
raise FxconvError(f"unknown character set '{charset}'")
if charset.count > grid.size(img):
raise FxconvError(f"not enough elements in grid (got {grid.size()}, "+
f"need {charset.count} for '{charset.name}'")
#--
# Proportionality and metadata
#--
proportional = (params.get("proportional", "false") == "true")
title = params.get("title", "")
if len(title) > 31:
raise FxconvError(f"font title {title} is too long (max. 31 bytes)")
# Pad title to 4 bytes
title = bytes(title, "utf-8") + bytes(((4 - len(title) % 4) % 4) * [0])
flags = set(params.get("flags", "").split(","))
flags.remove("")
flags_std = { "bold", "italic", "serif", "mono" }
if flags - flags_std:
raise FxconvError(f"unknown flags: {', '.join(flags - flags_std)}")
bold = int("bold" in flags)
italic = int("italic" in flags)
serif = int("serif" in flags)
mono = int("mono" in flags)
header = bytes([
(len(title) << 3) | (bold << 2) | (italic << 1) | serif,
(mono << 7) | (int(proportional) << 6) | (charset.id & 0xf),
params.get("height", grid.h),
grid.h,
])
encode16bit = lambda x: bytes([ x >> 8, x & 255 ])
fixed_header = encode16bit(grid.w) + encode16bit((grid.w*grid.h + 31) >> 5)
#--
# Encoding glyphs
#--
data_glyphs = []
data_widths = bytearray()
data_index = bytearray()
for (number, region) in enumerate(grid.iter(img)):
# Upate index
if not (number % 8):
idx = len(data_glyphs) // 4
data_index += encode16bit(idx)
# Get glyph area
glyph = img.crop(region)
glyph.save(f"/tmp/img{number}.png")
if proportional:
glyph = _trim(glyph)
data_widths.append(glyph.width)
length = 4 * ((glyph.width * glyph.height + 31) >> 5)
bits = bytearray(length)
offset = 0
px = glyph.load()
for y in range(glyph.size[1]):
for x in range(glyph.size[0]):
color = (px[x,y] == FX_BLACK)
bits[offset >> 3] |= ((color * 0x80) >> (offset & 7))
offset += 1
data_glyphs.append(bits)
data_glyphs = b''.join(data_glyphs)
#---
# Object file generation
#---
if proportional:
data = header + data_index + data_widths + data_glyphs + title
else:
data = header + fixed_header + data_glyphs + title
elf(data, output, "_" + params["name"])
#
# Exceptions
#
FxconvError = Exception
#
# API
#
def quantize(img, dither=False):
"""
Convert a PIL.Image.Image into an RGBA image whose only colors are:
* FX_BLACK = ( 0, 0, 0, 255)
* FX_DARK = ( 85, 85, 85, 255)
* FX_LIGHT = (170, 170, 170, 255)
* FX_WHITE = (255, 255, 255, 255)
* FX_ALPHA = ( 0, 0, 0, 0)
The alpha channel is first flattened to either opaque of full transparent,
then all colors are quantized into the 4-shade scale. Floyd-Steinberg
dithering can be used, although most applications will prefer nearest-
neighbor coloring.
Arguments:
img -- Input image, in any format
dither -- Enable Floyd-Steinberg dithering [default: False]
Returns a quantized PIL.Image.Image.
"""
# Our palette will have only 4 colors for the gray engine
colors = [ FX_BLACK, FX_DARK, FX_LIGHT, FX_WHITE ]
# Create the palette
palette = Image.new("RGBA", (len(colors), 1))
for (i, c) in enumerate(colors):
palette.putpixel((i, 0), c)
palette = palette.convert("P")
palette.save("/tmp/palette.png")
# Save the alpha channel, and make it either full transparent or opaque
try:
alpha_channel = img.getchannel("A").convert("1", dither=Image.NONE)
except:
alpha_channel = Image.new("L", img.size, 255)
# Apply the palette to the original image (transparency removed)
img = img.convert("RGB")
# Let's do an equivalent of the following, but with a dithering setting:
# img = img.quantize(palette=palette)
img.load()
palette.load()
im = img.im.convert("P", int(dither), palette.im)
img = img._new(im)
# Put back the alpha channel
img.putalpha(alpha_channel)
# Premultiply alpha
pixels = img.load()
for y in range(img.size[1]):
for x in range(img.size[0]):
r, g, b, a = pixels[x, y]
if a == 0:
r, g, b, = 0, 0, 0
pixels[x, y] = (r, g, b, a)
return img
def convert(input, params, output=None):
"""
Convert a data file into an object that exports the following symbols:
* _<varname>
* _<varname>_end
* _<varname>_size
The variable name is obtained from the parameter dictionary <params>.
Arguments:
input -- Input file path
params -- Parameter dictionary
output -- Output file name [default: <input> with suffix '.o']
Produces an output file and returns nothing.
"""
if output is None:
output = os.path.splitext(input)[0] + '.o'
if "name" not in params:
raise FxconvError(f"no name specified for conversion '{input}'")
if "type" not in params:
raise FxconvError(f"missing type in conversion '{input}'")
elif params["type"] == "binary":
_convert_binary(input, output, params)
elif params["type"] == "image":
_convert_image(input, output, params)
elif params["type"] == "font":
_convert_font(input, output, params)
def elf(data, output, symbol, section=None, arch="sh3"):
"""
Call objcopy to create an object file from the specified data. The object
file will export three symbols:
* <symbol>
* <symbol>_end
* <symbol>_size
The symbol name must have a leading underscore if it is to be declared and
used from a C program.
The section name can be specified, along with its flags. A typical example
would be section=".rodata,contents,alloc,load,readonly,data", which is the
default.
The architecture can be either "sh3" or "sh4". This affects the choice of
the toolchain (sh3eb-elf-objcopy versus sh4eb-nofpu-elf-objcopy) and the
--binary-architecture flag of objcopy.
Arguments:
data -- A bytes-like object with data to embed into the object file
output -- Name of output file
symbol -- Chosen symbol name
section -- Target section [default: above variation of .rodata]
arch -- Target architecture: "sh3" or "sh4" [default: "sh3"]
Produces an output file and returns nothing.
"""
toolchain = { "sh3": "sh3eb-elf", "sh4": "sh4eb-nofpu-elf" }[arch]
if section is None:
section = ".rodata,contents,alloc,load,readonly,data"
with tempfile.NamedTemporaryFile() as fp:
fp.write(data)
fp.flush()
sybl = "_binary_" + fp.name.replace("/", "_")
objcopy_args = [
f"{toolchain}-objcopy", "-I", "binary", "-O", "elf32-sh",
"--binary-architecture", arch, "--file-alignment", "4",
"--rename-section", f".data={section}",
"--redefine-sym", f"{sybl}_start={symbol}",
"--redefine-sym", f"{sybl}_end={symbol}_end",
"--redefine-sym", f"{sybl}_size={symbol}_size",
fp.name, output ]
proc = subprocess.run(objcopy_args)
if proc.returncode != 0:
raise FxconvError(f"objcopy returned {proc.returncode}")

+ 146
- 0
fxg1a/dump.c View File

@ -0,0 +1,146 @@
#include <stdio.h>
#include <string.h>
#include <endian.h>
#include <fxg1a.h>
#include <g1a.h>
/* check(): Check validity of a g1a control or fixed field
This function checks a single field of a g1a header (depending on the value
of @test, from 0 up) and returns:
* 0 if the field is valid
* 1 if there is a minor error (wrong fixed-byte entry)
* 2 if there is a major error (like not a g1a, bad checksum, etc)
* -1 if the value of @test is out of bounds
It produces a description of the check in @status (even if the test is
passed); the string should have room for at least 81 bytes.
@test Test number
@g1a G1A file being manipulated
@size File size
@status Array row, at least 81 bytes free */
static int check(int test, struct g1a const *g1a, size_t size, char *status)
{
#define m(msg, ...) sprintf(status, msg, ##__VA_ARGS__)
struct header const *h = &g1a->header;
uint8_t const *raw = (void *)h;
uint16_t sum;
uint8_t ctrl;
switch(test)
{
case 0:
m("Signature \"USBPower\" \"########\"");
strncpy(status + 28, h->magic, 8);
return strncmp(h->magic, "USBPower", 8) ? 2:0;
case 1:
m("MCS Type 0xf3 0x%02x", h->mcs_type);
return (h->mcs_type != 0xf3) ? 2:0;
case 2:
m("Sequence 1 0x0010001000 0x%02x%02x%02x%02x%02x",
h->seq1[0], h->seq1[1], h->seq1[2], h->seq1[3], h->seq1[4]);
return strncmp((const char *)h->seq1, "\x00\x01\x00\x01\x00",
5) ? 1:0;
case 3:
ctrl = raw[0x13] + 0x41;
m("Control 1 0x%02x 0x%02x", ctrl, h->control1);
return (h->control1 != ctrl) ? 2:0;
case 4:
m("Sequence 2 0x01 0x%02x", h->seq2);
return (h->seq2 != 0x01) ? 1:0;
case 5:
m("File size 1 %-8zu %u", size,
be32toh(h->filesize_be1));
return (be32toh(h->filesize_be1) != size) ? 2:0;
case 6:
ctrl = raw[0x13] + 0xb8;
m("Control 2 0x%02x 0x%02x", ctrl, h->control2);
return (h->control2 != ctrl) ? 2:0;
case 7:
sum = checksum(g1a, size);
m("Checksum 0x%02x 0x%02x", sum,
be16toh(h->checksum));
return (be16toh(h->checksum) != sum) ? 2:0;
case 8:
m("File size 2 %-8zu %u", size,
be32toh(h->filesize_be2));
return (be32toh(h->filesize_be2) != size) ? 2:0;
default:
return -1;
}
}
/* unknown(): Print an unknown field
@data Address of field
@offset Offset of field in header
@size Number of consecutive unknown bytes */
static void unknown(uint8_t const *data, size_t offset, size_t size)
{
printf(" 0x%03zx %-4zd 0x", offset, size);
for(size_t i = 0; i < size; i++) printf("%02x", data[offset + i]);
printf("\n");
}
/* field(): Print a text field with limited size
@field Address of text field
@size Maximum number of bytes to print */
static void field(const char *field, size_t size)
{
for(size_t i = 0; i < size && field[i]; i++) putchar(field[i]);
printf("\n");
}
/* dump(): Print the detailed header fields of a g1a file */
void dump(struct g1a const *g1a, size_t size)
{
struct header const *header = &g1a->header;
uint8_t const *raw = (void *)header;
/* Checks for g1a files */
char status[81];
int ret = 0;
int passed = 0;
printf("G1A signature checks:\n\n");
printf(" Sta. Field Expected Value\n");
for(int test = 0; ret >= 0; test++)
{
ret = check(test, g1a, size, status);
passed += !ret;
if(ret < 0) break;
printf(" %s %s\n", ret ? "FAIL" : "OK ", status);
}
printf("\nFields with unknown meanings:\n\n");
printf(" Offset Size Value\n");
unknown(raw, 0x015, 1);
unknown(raw, 0x018, 6);
unknown(raw, 0x028, 3);
unknown(raw, 0x02c, 4);
unknown(raw, 0x03a, 2);
unknown(raw, 0x04a, 2);
unknown(raw, 0x1d0, 4);
unknown(raw, 0x1dc, 20);
unknown(raw, 0x1f4, 12);
printf("\nApplication metadata:\n\n");
printf(" Program name: ");
field(header->name, 8);
printf(" Internal name: ");
field(header->internal, 8);
printf(" Version: ");
field(header->version, 10);
printf(" Build date: ");
field(header->date, 14);
printf("\nProgram icon:\n\n");
icon_print(header->icon);
}

+ 71
- 0
fxg1a/edit.c View File

@ -0,0 +1,71 @@
#include <fxg1a.h>
#include <stdio.h>
#include <string.h>
/* sign(): Sign header by filling fixed fields and checksums */
void sign(struct g1a *g1a, size_t size)
{
struct header *header = &g1a->header;
/* Fixed elements */
memcpy(header->magic, "USBPower", 8);
header->mcs_type = 0xf3;
memcpy(header->seq1, "\x00\x10\x00\x10\x00", 5);
header->seq2 = 0x01;
header->filesize_be1 = htobe32(size);
header->filesize_be2 = htobe32(size);
/* Control bytes and checksums */
header->control1 = size + 0x41;
header->control2 = size + 0xb8;
header->checksum = htobe16(checksum(g1a, size));
}
/* edit_name(): Set application name */
void edit_name(struct g1a *g1a, const char *name)
{
memset(g1a->header.name, 0, 8);
if(!name) return;
for(int i = 0; name[i] && i < 8; i++)
g1a->header.name[i] = name[i];
}
/* edit_internal(): Set internal name */
void edit_internal(struct g1a *g1a, const char *internal)
{
memset(g1a->header.internal, 0, 8);
if(!internal) return;
for(int i = 0; internal[i] && i < 8; i++)
g1a->header.internal[i] = internal[i];
}
/* edit_version(): Set version string */
void edit_version(struct g1a *g1a, const char *version)
{
memset(g1a->header.version, 0, 10);
if(!version) return;
for(int i = 0; version[i] && i < 10; i++)
g1a->header.version[i] = version[i];
}
/* edit_date(): Set build date */
void edit_date(struct g1a *g1a, const char *date)
{
memset(g1a->header.date, 0, 14);
if(!date) return;
for(int i = 0; date[i] && i < 14; i++)
g1a->header.date[i] = date[i];
}
/* edit_icon(): Set icon from monochrome bitmap */
void edit_icon(struct g1a *g1a, uint8_t const *mono)
{
memcpy(g1a->header.icon, mono, 68);
}

+ 111
- 0
fxg1a/file.c View File

@ -0,0 +1,111 @@
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fxg1a.h>
/* invert_header(): Bit-invert a standard header
Part of the header is stored inverted in files for obfuscation purposes. */
static void invert_header(struct g1a *g1a)
{
uint8_t *data = (void *)&g1a->header;
for(size_t i = 0; i < 0x20; i++) data[i] = ~data[i];
}
#define fail(msg, ...) { \
fprintf(stderr, "error: " msg ": %m\n", ##__VA_ARGS__); \
close(fd); \
free(data); \
return NULL; \
}
/* load(): Fully load a file into memory
Allocates a buffer with @prepend leading bytes initialized to zero. */
static void *load(const char *filename, size_t *size, size_t prepend)
{
int fd;
struct stat statbuf;
void *data = NULL;
size_t filesize;
fd = open(filename, O_RDONLY);
if(fd < 0) fail("cannot open %s", filename);
int x = fstat(fd, &statbuf);
if(x > 0) fail("cannot stat %s", filename);
filesize = statbuf.st_size;
data = malloc(prepend + filesize);
if(!data) fail("cannot load %s", filename);
size_t remaining = filesize;
while(remaining > 0)
{
size_t offset = prepend + filesize - remaining;
ssize_t y = read(fd, data + offset, remaining);
if(y < 0) fail("cannot read from %s", filename);
remaining -= y;
}
close(fd);
memset(data, 0, prepend);
if(size) *size = prepend + filesize;
return data;
}
/* load_g1a(): Load a g1a file into memory */
struct g1a *load_g1a(const char *filename, size_t *size)
{
struct g1a *ret = load(filename, size, 0);
if(ret) invert_header(ret);
return ret;
}
/* load_binary(): Load a binary file into memory */
struct g1a *load_binary(const char *filename, size_t *size)
{
struct g1a *ret = load(filename, size, 0x200);
if(ret) memset(ret, 0xff, 0x20);
return ret;
}
#undef fail
#define fail(msg, ...) { \
fprintf(stderr, "error: " msg ": %m\n", ##__VA_ARGS__); \
close(fd); \
invert_header(g1a); \
return 1; \
}
/* save_g1a(): Save a g1a file to disk */
int save_g1a(const char *filename, struct g1a *g1a, size_t size)
{
/* Invert header before saving */
invert_header(g1a);
int fd = creat(filename, 0644);
if(fd < 0) fail("cannot open %s", filename);
void const *raw = g1a;
ssize_t status;
size_t written = 0;
while(written < size)
{
status = write(fd, raw + written, size - written);
if(status < 0) fail("cannot write to %s", filename);
written += status;
}
close(fd);
/* Before returning, re-invert header for further use */
invert_header(g1a);
return 0;
}

+ 168
- 0
fxg1a/fxg1a.h View File

@ -0,0 +1,168 @@
//---
// fxg1a:fxg1a - Main interfaces
//---
#ifndef FX_FXG1A
#define FX_FXG1A
#include <stdlib.h>
#include <stdint.h>
#include <stddef.h>
#include <g1a.h>
/*
** Header dumping (dump.c)
*/
/* dump(): Print the detailed header fields of a g1a file
This function takes as argument the full file loaded into memory and the
size of the file. It does various printing to stdout as main job.
@g1a Full file data
@size Size of g1a file */
void dump(struct g1a const *g1a, size_t size);
/*
** Header manipulation (edit.c)
*/
/* sign(): Sign header by filling fixed fields and checksums
This function fills the fixed fields and various checksums of a g1a file. To
do this it accesses some of the binary data. To set the user-customizable
field, use the edit_*() functions. (The value of the customizable fields
does not influence the checksums so it's okay to not call this function
afterwards.)
@g1a Header to sign
@size Size of raw file data */
void sign(struct g1a *g1a, size_t size);
/* edit_*(): Set various fields of a g1a header */
void edit_name (struct g1a *g1a, const char *name);
void edit_internal (struct g1a *g1a, const char *internal);
void edit_version (struct g1a *g1a, const char *version);
void edit_date (struct g1a *g1a, const char *date);
/* edit_icon(): Set monochrome icon of a g1a header
The icon parameter must be loaded in 1-bit bitmap format. */
void edit_icon(struct g1a *header, uint8_t const *mono);
/*
** Utility functions (util.c)
*/
/* checksum(): Sum of 8 big-endian shorts at 0x300
Computes the third checksum by summing bytes from the data part of the file.
@g1a Add-in file whose checksum is requested
@size Size of file */
uint16_t checksum(struct g1a const *g1a, size_t size);
/* default_output(): Calculate default output file name
This function computes a default file name by replacing the extension of
@name (if it exists) or adding one. The extension is specified as a suffix,
usually in the form ".ext".
The resulting string might be as long as the length of @name plus that of
@suffix (plus one NUL byte); the provided buffer must point to a suitably-
large allocated space.
@name Input file name
@suffix Suffix to add or replace @name's extension with
@output Output file name */
void default_output(const char *name, const char *suffix, char *output);
/*
** File manipulation (file.c)
*/
/* load_g1a(): Load a g1a file into memory
This function loads @filename into a dynamically-allocated buffer and
returns the address of that buffer; it must be free()'d after use. When
loading the file, if @size is not NULL, it receives the size of the file.
On error, load() prints a message an stderr and returns NULL. The header
is inverted before this function returns.
@filename File to load
@size If non-NULL, receives the file size
Returns a pointer to a buffer with loaded data, or NULL on error. */
struct g1a *load_g1a(const char *filename, size_t *size);
/* load_binary(): Load a binary file into memory
This function operates like load_g1a() but reserves space for an empty
header. The header is initialized with all zeros.
@filename File to load
@size If non-NULL, receives the file size
Returns a pointer to a buffer with loaded data, or NULL on error. */
struct g1a *load_binary(const char *filename, size_t *size);
/* save_g1a(): Save a g1a file to disk
This functions creates @filename, then writes a g1a header and a chunk of
raw data to it. Since it temporarily inverts the header to comply with
Casio's obfuscated format, it needs write access to @g1a. Returns non-zero
on error.
@filename File to write (it will be overridden if it exists)
@g1a G1A data to write
@size Size of data
Returns zero on success and a nonzero error code otherwise. */
int save_g1a(const char *filename, struct g1a *g1a, size_t size);
/*
** Icon management (icon.c)
*/
/* icon_print(): Show a monochrome 30*17 icon on stdout
The buffer should point to a 68-byte array. */
void icon_print(uint8_t const *icon);
/* icon_load(): Load a monochrome PNG image into an array
This function loads a PNG image into a 1-bit buffer; each row is represented
by a fixed number of bytes, each byte being 8 pixels. Rows are loaded from
top to bottom, and from left to right.
If the image is not a PNG image or a reading error occurs, this functions
prints an error message on stderr and returns NULL.
@filename PNG file to load
@width If non-NULL, receives image width
@height If non-NULL, receives image height
Returns a pointer to a free()able buffer with loaded data, NULL on error. */
uint8_t *icon_load(const char *filename, size_t *width, size_t *height);
/* icon_save(): Save an 8-bit array to a PNG image
Assumes 8-bit GRAY format.
@filename Target filename
@input An 8-bit GRAY image
@width Width of input, should be equal to stride
@height Height of input
Returns non-zero on error. */
int icon_save(const char *filename, uint8_t *input, size_t width,
size_t height);
/* icon_conv_8to1(): Convert an 8-bit icon to 1-bit
The returned 1-bit icon is always of size 30*17, if the input size does not
match it is adjusted.
@input 8-bi data
@width Width of input image
@height Height of input image
Returns a free()able buffer with a 1-bit icon on success, NULL on error. */
uint8_t *icon_conv_8to1(uint8_t const *input, size_t width, size_t height);
/* icon_conv_1to8(): Convert an 1-bit icon to 8-bit
Input 1-bit is assumed to be 30*17 in size, this function returns an 8-bit
buffer with the same dimensions.
@mono Input monochrome icon (from a g1a header, for instance)
Returns a free()able buffer, or NULL on error. */
uint8_t *icon_conv_1to8(uint8_t const *mono);
#endif /* FX_FXG1A */

+ 59
- 0
fxg1a/g1a.h View File

@ -0,0 +1,59 @@
//---
// fxg1a:g1a - Add-in header for Casio's G1A format
//---
#ifndef FX_G1A
#define FX_G1A
#include <stdint.h>
/* TODO: eStrips are not supported yet */
struct estrip
{
uint8_t data[80];
};
/* G1A file header with 0x200 bytes. When output to a file the standard part
(first 0x20 bytes) of this header is bit-inverted, but the non-inverted
version makes a lot more sens so we'll be using it. */
struct header
{ /* Offset Size Value */
char magic[8]; /* 0x000 8 "USBPower" */
uint8_t mcs_type; /* 0x008 1 0xf3 (AddIn) */
uint8_t seq1[5]; /* 0x009 5 0x0010001000 */
uint8_t control1; /* 0x00e 1 *0x13 + 0x41 */
uint8_t seq2; /* 0x00f 1 0x01 */
uint32_t filesize_be1; /* 0x010 4 File size, big endian */
uint8_t control2; /* 0x014 1 *0x13 + 0xb8 */
uint8_t _1; /* 0x015 1 ??? */
uint16_t checksum; /* 0x016 2 BE sum of 8 shorts at 0x300 */
uint8_t _2[6]; /* 0x018 6 ??? */
uint16_t mcs_objects; /* 0x01e 2 MCS-only, unused */
char internal[8]; /* 0x020 8 Internal app name with '@' */
uint8_t _3[3]; /* 0x028 3 ??? */
uint8_t estrips; /* 0x02b 1 Number of estrips (0..4) */
uint8_t _4[4]; /* 0x02c 4 ??? */
char version[10]; /* 0x030 10 Version "MM.mm.pppp" */
uint8_t _5[2]; /* 0x03a 2 ??? */
char date[14]; /* 0x03c 14 Build date "yyyy.MMdd.hhmm" */
uint8_t _6[2]; /* 0x04a 2 ??? */
uint8_t icon[68]; /* 0x04c 68 30*17 monochrome icon */
struct estrip estrip1; /* 0x090 80 eStrip 1 */
struct estrip estrip2; /* 0x0e0 80 eStrip 2 */
struct estrip estrip3; /* 0x130 80 eStrip 3 */
struct estrip estrip4; /* 0x180 80 eStrip 4 */
uint8_t _7[4]; /* 0x1d0 4 ??? */
char name[8]; /* 0x1d4 8 Add-in name */
uint8_t _8[20]; /* 0x1dc 20 ??? */
uint32_t filesize_be2; /* 0x1f0 4 File size, big endian */
uint8_t _9[12]; /* 0x1f4 12 ??? */
};
/* A full g1a file, suitable for use with pointers */
struct g1a
{
struct header header;
uint8_t code[];
};
#endif /* FX_G1A */

+ 132
- 0
fxg1a/icon.c View File

@ -0,0 +1,132 @@
#include <stdio.h>
#include <string.h>
#include <fxg1a.h>
#include <png.h>
/* icon_print(): Show a monochrome 30*17 icon on stdout */
void icon_print(uint8_t const *icon)
{
for(int y = 0; y < 17; y++)
{
for(int x = 0; x < 30; x++)
{
int v = icon[(y << 2) + (x >> 3)] & (0x80 >> (x & 7));
putchar(v ? '#' : ' ');
putchar(v ? '#' : ' ');
}
putchar('\n');
}
}
/* icon_load(): Load a monochrome PNG image into an array */
uint8_t *icon_load(const char *filename, size_t *width, size_t *height)
{
png_image img;
memset(&img, 0, sizeof img);
img.opaque = NULL;
img.version = PNG_IMAGE_VERSION;
png_image_begin_read_from_file(&img, filename);
if(img.warning_or_error)
{
fprintf(stderr, "libpng %s: %s\n", img.warning_or_error == 1
? "warning": "error", img.message);
if(img.warning_or_error > 1)
{
png_image_free(&img);
return NULL;
}
}
img.format = PNG_FORMAT_GRAY;
void *buffer = calloc(img.width * img.height, 1);
if(!buffer)
{
fprintf(stderr, "error: cannot read %s: %m\n", filename);
png_image_free(&img);
return NULL;
}
png_image_finish_read(&img, NULL, buffer, img.width, NULL);
if(width) *width = img.width;
if(height) *height = img.height;
png_image_free(&img);
return buffer;
}
/* icon_save(): Save an 8-bit array to a PNG image */
int icon_save(const char *filename, uint8_t *input, size_t width,
size_t height)
{
png_image img;
memset(&img, 0, sizeof img);
img.version = PNG_IMAGE_VERSION;
img.width = width;
img.height = height;
img.format = PNG_FORMAT_GRAY;
png_image_write_to_file(&img, filename, 0, input, 0, NULL);
png_image_free(&img);
if(img.warning_or_error)
{
fprintf(stderr, "libpng %s: %s\n", img.warning_or_error == 1
? "warning": "error", img.message);
if(img.warning_or_error > 1) return 1;
}
return 0;
}
/* icon_conv_8to1(): Convert an 8-bit icon to 1-bit */
uint8_t *icon_conv_8to1(uint8_t const *input, size_t width, size_t height)
{
if(!input) return NULL;
uint8_t *mono = calloc(68, 1);
if(!mono) return NULL;
size_t stride = width;
/* If the image is wider than 30 pixels, ignore columns at the right */
if(width > 30) width = 30;
/* Skip the first line if there is enough rows, because in standard
30*19 icons, the first and last lines are skipped */
if(height > 17) input += stride, height--;
/* If height is still larger than 17, ignore rows at the bottom */
if(height > 17) height = 17;
/* Then copy individual pixels on the currently-blank image */
for(size_t y = 0; y < height; y++)
for(size_t x = 0; x < width; x++)
{
int offset = (y << 2) + (x >> 3);
int color = input[y * stride + x] < 128;
uint8_t mask = color << (~x & 7);
mono[offset] |= mask;
}
return mono;
}
/* icon_conv_1to8(): Convert an 1-bit icon to 8-bit */
uint8_t *icon_conv_1to8(uint8_t const *mono)
{
uint8_t *data = calloc(30 * 17, 1);
if(!data) return NULL;
for(int y = 0; y < 17; y++)
for(int x = 0; x < 30; x++)
{
int offset = (y << 2) + (x >> 3);
int bit = mono[offset] & (0x80 >> (x & 7));
data[y * 30 + x] = (bit ? 0x00 : 0xff);
}
return data;
}

+ 266
- 0
fxg1a/main.c View File

@ -0,0 +1,266 @@
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <getopt.h>
#include <fxg1a.h>
#include <g1a.h>
static const char *help_string =
"usage: %1$s [-g] <binary file> [options...]\n"
" %1$s -e <g1a file> [options...]\n"
" %1$s -d <g1a file>\n"
" %1$s -r <g1a file> [-o <g1a file>]\n"
" %1$s -x <g1a file> [-o <png file>]\n"
"\n"
"fxg1a creates or edits g1a files (add-in applications for Casio fx9860g\n"
"calculator series) that consist of a g1a header followed by binary code.\n"
"\n"
"Operating modes:\n"
" -g, --g1a Generate a g1a file (default)\n"
" -e, --edit Edit header of an existing g1a file\n"
" -d, --dump Dump header of an existing g1a file\n"
" -r, --repair Recalculate control bytes and checksums\n"
" -x, --extract Extract icon into a PNG file\n"
"\n"
"General options:\n"
" -o, --output=<file> Output file (default: input file with .g1a suffix\n"
" [-g]; with .png suffix [-x]; input file [-e, -r])\n"
"\n"
"Generation and edition options:\n"
" -i, --icon=<png> Program icon, in PNG format (default: blank icon)\n"
" -n, --name=<name> Add-in name, 8 bytes (default: output file name)\n"
" --version=<text> Program version, MM.mm.pppp format (default: empty)\n"
" --internal=<name> Internal name, eg. '@NAME' (default: empty)\n"
" --date=<date> Date of build, yyyy.MMdd.hhmm (default: now)\n";
/*
** Field customization
*/
/* A set of user-defined fields, often taken on the command-line
Default values are NULL and indicate "no value" (-g) or "no change" (-e). */
struct fields
{
/* New values for basic fields */
const char *name;
const char *version;
const char *internal;
const char *date;
/* Icon file name */
const char *icon;
};
/* fields_edit(): Set the value of some fields altogether
@header Header to edit, is assumed checksumed and filled
@fields New values for fields, any members can be NULL */
void fields_edit(struct g1a *header, struct fields const *fields)
{
/* For easy fields, just call the appropriate edition function */
if(fields->name) edit_name(header, fields->name);
if(fields->version) edit_version(header, fields->version);
if(fields->internal) edit_internal(header, fields->internal);
if(fields->date) edit_date(header, fields->date);
/* Load icon from PNG file */
if(fields->icon)
{
size_t width, height;
uint8_t *data = icon_load(fields->icon, &width, &height);
if(!data) return;