More minor API cleanup.
This commit is contained in:
parent
ac44ac1303
commit
e228efba2a
15 changed files with 221 additions and 19 deletions
36
PERF.md
Normal file
36
PERF.md
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
| Op | IIgs (ops/sec) | Amiga (vs IIGS) | Atari ST (vs IIGS) | DOS (vs IIGS) |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| surfaceClear | 28 | 2.54x | 1.11x | 638.57x |
|
||||||
|
| paletteSet | 678 | 8.68x | 4.23x | 26.13x |
|
||||||
|
| scbSetRange | 1005 | 3.66x | 1.86x | 11.16x |
|
||||||
|
| drawPixel | 1755 | 1.85x | 1.01x | 9.70x |
|
||||||
|
| drawLine H | 682 | 2.19x | 1.22x | 19.26x |
|
||||||
|
| drawLine V | 90 | 1.67x | 1.11x | 115.30x |
|
||||||
|
| drawLine diag | 35 | 1.14x | 1.06x | 261.49x |
|
||||||
|
| drawRect 100x100 | 75 | 1.57x | 1.04x | 250.77x |
|
||||||
|
| drawCircle r=16 | 232 | 1.21x | 0.65x | 71.24x |
|
||||||
|
| drawCircle r=80 | 56 | 1.16x | 0.61x | 310.93x |
|
||||||
|
| fillRect 16x16 | 450 | 1.24x | 1.04x | 39.38x |
|
||||||
|
| fillRect 80x80 | 75 | 0.95x | 1.28x | 206.73x |
|
||||||
|
| fillRect 320x200 | 60 | 0.93x | 0.43x | 184.62x |
|
||||||
|
| fillCircle r=40 | 38 | 0.97x | 1.39x | 347.24x |
|
||||||
|
| samplePixel | 1916 | 3.48x | 1.92x | 18.04x |
|
||||||
|
| tileFill | 1252 | 1.73x | 0.93x | 10.44x |
|
||||||
|
| tileCopy | 997 | 1.80x | 1.02x | 16.96x |
|
||||||
|
| tileCopyMasked | 498 | 1.76x | 1.08x | 28.55x |
|
||||||
|
| tilePaste | 1106 | 1.94x | 1.09x | 13.53x |
|
||||||
|
| tileSnap | 1473 | 2.26x | 1.28x | 9.44x |
|
||||||
|
| spriteSaveUnder | 528 | 2.21x | 1.29x | 20.14x |
|
||||||
|
| spriteDraw | 438 | 1.82x | 1.28x | 38.39x |
|
||||||
|
| spriteRestoreUnder | 487 | 2.00x | 1.11x | 36.71x |
|
||||||
|
| spriteSaveAndDraw | 277 | 1.82x | 1.06x | 72.79x |
|
||||||
|
| stagePresent full | 42 | 6.31x | 1.40x | 373.83x |
|
||||||
|
| joeyInputPoll | 273 | 13.90x | 4.19x | 57.15x |
|
||||||
|
| joeyKeyDown | 3382 | 7.82x | 3.76x | 18.93x |
|
||||||
|
| joeyKeyPressed | 3345 | 7.65x | 3.81x | 18.72x |
|
||||||
|
| joeyMouseX | 4170 | 10.26x | 5.22x | 26.27x |
|
||||||
|
| joeyJoyConnected | 3378 | 7.76x | 3.71x | 18.95x |
|
||||||
|
| joeyAudioFrameTick | 4106 | 7.99x | 3.04x | 12.37x |
|
||||||
|
| joeyAudioIsPlayingMod | 3536 | 9.28x | 4.33x | 26.78x |
|
||||||
|
| surfaceMarkDirtyRect (via fillRect 32x32) | 240 | 1.38x | 1.26x | 58.95x |
|
||||||
|
| c2p full screen | - | - | - | - |
|
||||||
|
|
@ -76,7 +76,6 @@ is a quick reference. Every entry point is plain C, no C++ extensions.
|
||||||
|
|
||||||
```c
|
```c
|
||||||
typedef struct {
|
typedef struct {
|
||||||
HostModeE hostMode; // HOST_MODE_TAKEOVER or HOST_MODE_OS
|
|
||||||
uint32_t codegenBytes; // runtime compiled-sprite cache size
|
uint32_t codegenBytes; // runtime compiled-sprite cache size
|
||||||
uint16_t maxSurfaces; // maximum concurrent surfaces
|
uint16_t maxSurfaces; // maximum concurrent surfaces
|
||||||
uint32_t audioBytes; // audio sample / module RAM pool
|
uint32_t audioBytes; // audio sample / module RAM pool
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,6 @@ int main(void) {
|
||||||
uint8_t *sfxBytes;
|
uint8_t *sfxBytes;
|
||||||
uint32_t sfxLen;
|
uint32_t sfxLen;
|
||||||
|
|
||||||
config.hostMode = HOST_MODE_TAKEOVER;
|
|
||||||
config.codegenBytes = 8 * 1024;
|
config.codegenBytes = 8 * 1024;
|
||||||
config.maxSurfaces = 4;
|
config.maxSurfaces = 4;
|
||||||
config.audioBytes = 64UL * 1024;
|
config.audioBytes = 64UL * 1024;
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,7 @@
|
||||||
// disk at center).
|
// disk at center).
|
||||||
// BR: tileCopy / tileCopyMasked / tileSnap+tilePaste / floodFill.
|
// BR: tileCopy / tileCopyMasked / tileSnap+tilePaste / floodFill.
|
||||||
//
|
//
|
||||||
// Runs in HOST_MODE_TAKEOVER and holds the frame until the user
|
// Holds the frame until the user presses ESC / RETURN / SPACE.
|
||||||
// presses ESC / RETURN / SPACE.
|
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
|
||||||
|
|
@ -243,7 +242,6 @@ int main(void) {
|
||||||
JoeyConfigT config;
|
JoeyConfigT config;
|
||||||
SurfaceT *screen;
|
SurfaceT *screen;
|
||||||
|
|
||||||
config.hostMode = HOST_MODE_TAKEOVER;
|
|
||||||
config.codegenBytes = 8 * 1024;
|
config.codegenBytes = 8 * 1024;
|
||||||
config.maxSurfaces = 4;
|
config.maxSurfaces = 4;
|
||||||
config.audioBytes = 64UL * 1024;
|
config.audioBytes = 64UL * 1024;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// JoeyLib hello-world example.
|
// JoeyLib hello-world example.
|
||||||
//
|
//
|
||||||
// Validates that the C API headers, library, and per-platform link
|
// Validates that the C API headers, library, and per-platform link
|
||||||
// path all work end-to-end. Runs in HOST_MODE_OS so it can use stdio.
|
// path all work end-to-end.
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <joey/joey.h>
|
#include <joey/joey.h>
|
||||||
|
|
@ -9,7 +9,6 @@
|
||||||
int main(void) {
|
int main(void) {
|
||||||
JoeyConfigT config;
|
JoeyConfigT config;
|
||||||
|
|
||||||
config.hostMode = HOST_MODE_OS;
|
|
||||||
config.codegenBytes = 8 * 1024;
|
config.codegenBytes = 8 * 1024;
|
||||||
config.maxSurfaces = 4;
|
config.maxSurfaces = 4;
|
||||||
config.audioBytes = 64UL * 1024;
|
config.audioBytes = 64UL * 1024;
|
||||||
|
|
|
||||||
|
|
@ -217,7 +217,6 @@ int main(void) {
|
||||||
JoeyConfigT config;
|
JoeyConfigT config;
|
||||||
SurfaceT *screen;
|
SurfaceT *screen;
|
||||||
|
|
||||||
config.hostMode = HOST_MODE_TAKEOVER;
|
|
||||||
config.codegenBytes = 8 * 1024;
|
config.codegenBytes = 8 * 1024;
|
||||||
config.maxSurfaces = 4;
|
config.maxSurfaces = 4;
|
||||||
config.audioBytes = 64UL * 1024;
|
config.audioBytes = 64UL * 1024;
|
||||||
|
|
|
||||||
|
|
@ -217,7 +217,6 @@ int main(void) {
|
||||||
int16_t cursorCol;
|
int16_t cursorCol;
|
||||||
int16_t cursorRow;
|
int16_t cursorRow;
|
||||||
|
|
||||||
config.hostMode = HOST_MODE_TAKEOVER;
|
|
||||||
config.codegenBytes = 8 * 1024;
|
config.codegenBytes = 8 * 1024;
|
||||||
config.maxSurfaces = 4;
|
config.maxSurfaces = 4;
|
||||||
config.audioBytes = 64UL * 1024;
|
config.audioBytes = 64UL * 1024;
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,6 @@ int main(void) {
|
||||||
JoeyConfigT config;
|
JoeyConfigT config;
|
||||||
SurfaceT *screen;
|
SurfaceT *screen;
|
||||||
|
|
||||||
config.hostMode = HOST_MODE_TAKEOVER;
|
|
||||||
config.codegenBytes = 8 * 1024;
|
config.codegenBytes = 8 * 1024;
|
||||||
config.maxSurfaces = 4;
|
config.maxSurfaces = 4;
|
||||||
config.audioBytes = 64UL * 1024;
|
config.audioBytes = 64UL * 1024;
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,6 @@ int main(void) {
|
||||||
int16_t vy;
|
int16_t vy;
|
||||||
bool haveBackup;
|
bool haveBackup;
|
||||||
|
|
||||||
config.hostMode = HOST_MODE_TAKEOVER;
|
|
||||||
/* Amiga planar emits 8 pre-shifted DRAW variants per sprite (one
|
/* Amiga planar emits 8 pre-shifted DRAW variants per sprite (one
|
||||||
* per x % 8 alignment) so the codegen arena needs roughly 8x what
|
* per x % 8 alignment) so the codegen arena needs roughly 8x what
|
||||||
* the chunky two-shift case asks for. 32 KB fits a 16x16 ball
|
* the chunky two-shift case asks for. 32 KB fits a 16x16 ball
|
||||||
|
|
|
||||||
|
|
@ -294,7 +294,6 @@ int main(void) {
|
||||||
uint16_t elapsedFrames;
|
uint16_t elapsedFrames;
|
||||||
unsigned long elapsedMs;
|
unsigned long elapsedMs;
|
||||||
|
|
||||||
config.hostMode = HOST_MODE_TAKEOVER;
|
|
||||||
/* 32 KB fits the 8 pre-shifted DRAW variants the Amiga planar
|
/* 32 KB fits the 8 pre-shifted DRAW variants the Amiga planar
|
||||||
* compiled sprite emitter generates. UL on the multiply because
|
* compiled sprite emitter generates. UL on the multiply because
|
||||||
* ORCA-C's 16-bit int overflows on 32 * 1024. */
|
* ORCA-C's 16-bit int overflows on 32 * 1024. */
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@
|
||||||
#include "types.h"
|
#include "types.h"
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
HostModeE hostMode; // takeover or cooperate with host OS
|
|
||||||
uint32_t codegenBytes; // runtime compiled-sprite cache size
|
uint32_t codegenBytes; // runtime compiled-sprite cache size
|
||||||
uint16_t maxSurfaces; // maximum concurrent surfaces
|
uint16_t maxSurfaces; // maximum concurrent surfaces
|
||||||
uint32_t audioBytes; // audio sample and module RAM pool
|
uint32_t audioBytes; // audio sample and module RAM pool
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,6 @@
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
HOST_MODE_TAKEOVER,
|
|
||||||
HOST_MODE_OS
|
|
||||||
} HostModeE;
|
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
VIDEO_REGION_NTSC,
|
VIDEO_REGION_NTSC,
|
||||||
VIDEO_REGION_PAL,
|
VIDEO_REGION_PAL,
|
||||||
|
|
|
||||||
|
|
@ -56,8 +56,10 @@ workbench=$support/workbench.adf
|
||||||
work=$(mktemp -d -t joeylib-amiga.XXXXXX)
|
work=$(mktemp -d -t joeylib-amiga.XXXXXX)
|
||||||
# Preserve any diagnostic dumps Pattern writes to the virtual HD
|
# Preserve any diagnostic dumps Pattern writes to the virtual HD
|
||||||
# (copper.txt, etc.) before the temp dir is removed on script exit.
|
# (copper.txt, etc.) before the temp dir is removed on script exit.
|
||||||
|
# Also stage UBER's joeylog.txt back to build/amiga/bin/ so the
|
||||||
|
# tools/uber-perf-table picks it up at the standard path.
|
||||||
dump_keep=/tmp/joeylib-amiga-dump
|
dump_keep=/tmp/joeylib-amiga-dump
|
||||||
trap 'mkdir -p "$dump_keep"; cp "$work"/*.txt "$dump_keep"/ 2>/dev/null; rm -rf "$work"' EXIT
|
trap 'mkdir -p "$dump_keep"; cp "$work"/*.txt "$dump_keep"/ 2>/dev/null; cp "$work"/joeylog.txt "$bin_dir/joeylog.txt" 2>/dev/null; rm -rf "$work"' EXIT
|
||||||
|
|
||||||
mkdir -p "$work/s"
|
mkdir -p "$work/s"
|
||||||
# Stage every built binary (executable file at top of bin_dir, no
|
# Stage every built binary (executable file at top of bin_dir, no
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,22 @@
|
||||||
|
|
||||||
extern struct Screen *gScreen;
|
extern struct Screen *gScreen;
|
||||||
|
|
||||||
|
// ----- Empty pointer image -----
|
||||||
|
//
|
||||||
|
// Full-screen custom-screen games conventionally hide the system
|
||||||
|
// pointer; we install an all-zero sprite via SetPointer so Workbench's
|
||||||
|
// arrow doesn't drift over the playfield. Game code that needs a
|
||||||
|
// cursor draws its own from joeyMouseX/Y.
|
||||||
|
//
|
||||||
|
// Amiga hardware sprite data: 2 leading control words (VSTART/HSTART/
|
||||||
|
// VSTOP), one line of (planeA, planeB) data words, 2 trailing zero
|
||||||
|
// words as terminator. All zeros yields a transparent pointer. Must
|
||||||
|
// live in chip RAM since sprite DMA reads it.
|
||||||
|
|
||||||
|
#define EMPTY_POINTER_BYTES (6 * sizeof(UWORD))
|
||||||
|
|
||||||
|
static UWORD *gEmptyPointer = NULL;
|
||||||
|
|
||||||
// ----- Prototypes -----
|
// ----- Prototypes -----
|
||||||
|
|
||||||
static void drainMessages(void);
|
static void drainMessages(void);
|
||||||
|
|
@ -260,6 +276,12 @@ void halInputInit(void) {
|
||||||
if (gWindow != NULL) {
|
if (gWindow != NULL) {
|
||||||
gMouseX = (int16_t)(gWindow->Width / 2);
|
gMouseX = (int16_t)(gWindow->Width / 2);
|
||||||
gMouseY = (int16_t)(gWindow->Height / 2);
|
gMouseY = (int16_t)(gWindow->Height / 2);
|
||||||
|
|
||||||
|
gEmptyPointer = (UWORD *)AllocMem((ULONG)EMPTY_POINTER_BYTES,
|
||||||
|
(ULONG)(MEMF_CHIP | MEMF_CLEAR));
|
||||||
|
if (gEmptyPointer != NULL) {
|
||||||
|
SetPointer(gWindow, gEmptyPointer, 1, 16, 0, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// lowlevel.library shipped with AmigaOS 2.05+. If absent (very old
|
// lowlevel.library shipped with AmigaOS 2.05+. If absent (very old
|
||||||
|
|
@ -284,6 +306,13 @@ void halInputShutdown(void) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
drainMessages();
|
drainMessages();
|
||||||
|
if (gEmptyPointer != NULL) {
|
||||||
|
// Restore the system pointer before freeing our image so
|
||||||
|
// Intuition isn't holding a stale chip-RAM reference.
|
||||||
|
ClearPointer(gWindow);
|
||||||
|
FreeMem(gEmptyPointer, (ULONG)EMPTY_POINTER_BYTES);
|
||||||
|
gEmptyPointer = NULL;
|
||||||
|
}
|
||||||
CloseWindow(gWindow);
|
CloseWindow(gWindow);
|
||||||
gWindow = NULL;
|
gWindow = NULL;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
151
tools/uber-perf-table
Executable file
151
tools/uber-perf-table
Executable file
|
|
@ -0,0 +1,151 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Render a cross-port UBER perf table in Markdown.
|
||||||
|
|
||||||
|
Reads each port's joeylog.txt (the file UBER appends ops/sec lines to)
|
||||||
|
and emits a Markdown table comparing all ports against a reference.
|
||||||
|
The reference column shows absolute ops/sec; every other port shows a
|
||||||
|
ratio of (port / reference) so a glance tells you whether each target
|
||||||
|
is at, above, or below the reference.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
tools/uber-perf-table [--ref iigs] [--out PATH] [--repo PATH]
|
||||||
|
|
||||||
|
Defaults pull the 4 standard log paths under the repo's build/ tree;
|
||||||
|
pass --repo if running from elsewhere. The reference port (default
|
||||||
|
IIgs, which DESIGN.md calls the perf floor) defines the ratio
|
||||||
|
denominator. Ports whose log file is missing are listed in the table
|
||||||
|
header as "(no log)" and their columns are filled with "-".
|
||||||
|
|
||||||
|
Example, run from the repo root:
|
||||||
|
|
||||||
|
tools/uber-perf-table > PERF.md
|
||||||
|
|
||||||
|
To compare against a different reference (e.g. profile DOS as the
|
||||||
|
floor for a DOS-only project):
|
||||||
|
|
||||||
|
tools/uber-perf-table --ref dos
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
LINE_RE = re.compile(
|
||||||
|
r"UBER:\s+(?P<op>[^:]+):\s+\d+\s+iters\s+/\s+\d+\s+frames\s+=\s+(?P<ops>\d+)\s+ops/sec"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Per-port log path relative to the repo root, in the column order the
|
||||||
|
# table renders. Reference port appears first.
|
||||||
|
PORTS = [
|
||||||
|
("iigs", "IIgs", "build/iigs/bin/joeylog.txt"),
|
||||||
|
("amiga", "Amiga", "build/amiga/bin/joeylog.txt"),
|
||||||
|
("atarist", "Atari ST", "build/atarist/bin/joeylog.txt"),
|
||||||
|
("dos", "DOS", "build/dos/bin/JOEYLOG.TXT"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_log(path):
|
||||||
|
"""Return {op: ops/sec} from a UBER log. Last entry per op wins."""
|
||||||
|
perf = {}
|
||||||
|
with open(path) as f:
|
||||||
|
for line in f:
|
||||||
|
m = LINE_RE.search(line)
|
||||||
|
if m:
|
||||||
|
perf[m.group("op").strip()] = int(m.group("ops"))
|
||||||
|
return perf
|
||||||
|
|
||||||
|
|
||||||
|
def format_ratio(port_ops, ref_ops):
|
||||||
|
if port_ops is None or ref_ops is None or ref_ops == 0:
|
||||||
|
return "-"
|
||||||
|
ratio = port_ops / ref_ops
|
||||||
|
return f"{ratio:.2f}x"
|
||||||
|
|
||||||
|
|
||||||
|
def render_table(port_data, ops_in_order, ref_key):
|
||||||
|
headers = ["Op"]
|
||||||
|
for key, label, _ in PORTS:
|
||||||
|
if not port_data[key]["loaded"]:
|
||||||
|
label = f"{label} (no log)"
|
||||||
|
if key == ref_key:
|
||||||
|
headers.append(f"{label} (ops/sec)")
|
||||||
|
else:
|
||||||
|
headers.append(f"{label} (vs {ref_key.upper()})")
|
||||||
|
|
||||||
|
rows = [headers, ["---"] * len(headers)]
|
||||||
|
for op in ops_in_order:
|
||||||
|
row = [op]
|
||||||
|
ref_ops = port_data[ref_key]["perf"].get(op)
|
||||||
|
for key, _, _ in PORTS:
|
||||||
|
ops = port_data[key]["perf"].get(op)
|
||||||
|
if key == ref_key:
|
||||||
|
row.append(str(ops) if ops is not None else "-")
|
||||||
|
else:
|
||||||
|
row.append(format_ratio(ops, ref_ops))
|
||||||
|
rows.append(row)
|
||||||
|
|
||||||
|
return "\n".join("| " + " | ".join(r) + " |" for r in rows)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Render a cross-port UBER perf table in Markdown."
|
||||||
|
)
|
||||||
|
parser.add_argument("--ref", default="iigs",
|
||||||
|
choices=[p[0] for p in PORTS],
|
||||||
|
help="Reference port (default: iigs)")
|
||||||
|
parser.add_argument("--out", default=None,
|
||||||
|
help="Write table to this file instead of stdout")
|
||||||
|
parser.add_argument("--repo", default=None,
|
||||||
|
help="Repo root (default: parent of this script)")
|
||||||
|
args = parser.parse_args(argv[1:])
|
||||||
|
|
||||||
|
if args.repo is None:
|
||||||
|
args.repo = os.path.dirname(os.path.dirname(os.path.abspath(argv[0])))
|
||||||
|
|
||||||
|
port_data = {}
|
||||||
|
union_ops = []
|
||||||
|
seen = set()
|
||||||
|
for key, _, rel in PORTS:
|
||||||
|
path = os.path.join(args.repo, rel)
|
||||||
|
try:
|
||||||
|
perf = parse_log(path)
|
||||||
|
loaded = True
|
||||||
|
except OSError:
|
||||||
|
perf = {}
|
||||||
|
loaded = False
|
||||||
|
port_data[key] = {"perf": perf, "loaded": loaded, "path": path}
|
||||||
|
for op in perf:
|
||||||
|
if op not in seen:
|
||||||
|
seen.add(op)
|
||||||
|
union_ops.append(op)
|
||||||
|
|
||||||
|
if not port_data[args.ref]["loaded"]:
|
||||||
|
sys.stderr.write(
|
||||||
|
f"error: reference log missing: {port_data[args.ref]['path']}\n"
|
||||||
|
f" run UBER on {args.ref} first, then retry.\n"
|
||||||
|
)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
if not union_ops:
|
||||||
|
sys.stderr.write("error: no UBER ops parsed from any log\n")
|
||||||
|
return 2
|
||||||
|
|
||||||
|
# Use the reference port's op order so the table matches UBER's
|
||||||
|
# natural sequence (surfaceClear first, joeyAudioIsPlayingMod last).
|
||||||
|
ref_ops = list(port_data[args.ref]["perf"].keys())
|
||||||
|
extras = [op for op in union_ops if op not in port_data[args.ref]["perf"]]
|
||||||
|
ordered = ref_ops + extras
|
||||||
|
|
||||||
|
table = render_table(port_data, ordered, args.ref)
|
||||||
|
if args.out:
|
||||||
|
with open(args.out, "w") as f:
|
||||||
|
f.write(table + "\n")
|
||||||
|
else:
|
||||||
|
sys.stdout.write(table + "\n")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main(sys.argv))
|
||||||
Loading…
Add table
Reference in a new issue