Compare commits

..
1 Commits
Author SHA1 Message Date
Tyler Wilding 4560feba0b decomp: finish tippy 2021-08-07 22:55:12 -04:00
15875 changed files with 382292 additions and 7864486 deletions

No files matched your search

+1 -15
View File
@@ -1,18 +1,4 @@
---
BasedOnStyle: Chromium
ColumnLimit: 100
SortIncludes: true
IncludeBlocks: Regroup
IncludeCategories:
- Regex: '^<[a-zA-Z_\-\.]+>'
Priority: 1
- Regex: '^"[a-zA-Z_\-\.]+"'
Priority: 2
- Regex: '^(<|")common.*'
Priority: 3
- Regex: '^"(third-party|fmt|curl).*'
Priority: 4
SortPriority: 5
- Regex: '.*'
Priority: 5
SortPriority: 4
SortIncludes: false
-1
View File
@@ -5,7 +5,6 @@ indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true
charset = utf-8
# https://editorconfig-specification.readthedocs.io/
# https://docs.microsoft.com/en-us/visualstudio/ide/cpp-editorconfig-properties?view=vs-2019
-11
View File
@@ -1,15 +1,4 @@
# Ensure line endings are consistently 'LF'
* text=auto
third-party/**/* linguist-vendored
third-party/**/* linguist-generated
test/decompiler/reference/** linguist-vendored
# suppress from PR diffs - is just reviewing the same thing twice (if properly added to g_src)
test/decompiler/reference/** linguist-generated
goal_src/import/** linguist-vendored
goal_src/import/** linguist-generated
*.gc linguist-language=lisp
*.gd linguist-language=lisp
*.gs linguist-language=Scheme
*.gp linguist-language=Scheme
goal_src/engine/gfx/ocean/ocean-tables.gc linguist-vendored
-8
View File
@@ -1,8 +0,0 @@
blank_issues_enabled: true
contact_links:
- name: Feature Requests and Ideas
url: https://github.com/open-goal/jak-project/discussions/new/choose
about: Please use the discussion board to bring up feature requests and ideas. If there is interest in them, they can then be converted to an issue(s).
- name: Discord Help Channel
url: https://discord.gg/dPRCfsju3N
about: The best place for general help and asking questions.
@@ -1,74 +0,0 @@
name: "\U0001F41B Jak 1 Bug Report"
description: Create a bug report for Jak 1.
labels: ["bug", "jak1"]
body:
- type: checkboxes
attributes:
label: Acknowledgements
description: Take a couple minutes to help our maintainers work faster.
options:
- label: I have [searched](https://github.com/open-goal/jak-project/issues?q=is%3Aissue+is%3Aopen+label%3Abug+label%3Ajak1+sort%3Aupdated-desc+) for duplicate or closed bug reports
required: true
- label: I understand that I am supposed to provide my own legitimately obtained copy of the game
required: true
- type: textarea
attributes:
label: Describe the Bug
description: A clear and concise description of what the bug is. You may post screenshots or videos of the bug here.
validations:
required: true
- type: textarea
attributes:
label: How To Reproduce
description: Steps to reproduce the behavior. You can also post a video of it here.
validations:
required: true
- type: dropdown
attributes:
label: Does this problem occur on original hardware or PCSX2?
description: Some things that may seem like bugs are actually exactly how the original game behaved.
options:
- Yes, it's unique to OpenGOAL
- Didn't check
- Not needed, bug is obvious
validations:
required: true
- type: textarea
attributes:
label: Expected Behavior
description: A clear and concise description of the expected behavior.
placeholder: When I do X, Y should happen.
validations:
required: true
- type: textarea
attributes:
label: Environment Information
description: "You can upload the [Support Package](https://github.com/open-goal/launcher#asking-for-help) provided by the Launcher here, or you can provide the following information: CPU, GPU, OS Version, OpenGOAL Version (found in the window's title bar)"
validations:
required: true
- type: dropdown
attributes:
label: Game Version
options:
- NTSC 1.0 (black label)
- NTSC Greatest Hits version (red label)
- PAL
- JP
validations:
required: true
- type: dropdown
attributes:
label: Have you set the game to something other than `60fps`?
options:
- "No"
- "Yes"
validations:
required: true
@@ -1,74 +0,0 @@
name: "\U0001F41B Jak 2 Bug Report"
description: Create a bug report for Jak 2.
labels: ["bug", "jak2"]
body:
- type: checkboxes
attributes:
label: Acknowledgements
description: Take a couple minutes to help our maintainers work faster.
options:
- label: I have [searched](https://github.com/open-goal/jak-project/issues?q=is%3Aissue+is%3Aopen+label%3Abug+label%3Ajak2+sort%3Aupdated-desc+) for duplicate or closed bug reports
required: true
- label: I understand that I am supposed to provide my own legitimately obtained copy of the game
required: true
- type: textarea
attributes:
label: Describe the Bug
description: A clear and concise description of what the bug is. You may post screenshots or videos of the bug here.
validations:
required: true
- type: textarea
attributes:
label: How To Reproduce
description: Steps to reproduce the behavior. You can also post a video of it here.
validations:
required: true
- type: dropdown
attributes:
label: Does this problem occur on original hardware or PCSX2?
description: Some things that may seem like bugs are actually exactly how the original game behaved.
options:
- Yes, it's unique to OpenGOAL
- Didn't check
- Not needed, bug is obvious
validations:
required: true
- type: textarea
attributes:
label: Expected Behavior
description: A clear and concise description of the expected behavior.
placeholder: When I do X, Y should happen.
validations:
required: true
- type: textarea
attributes:
label: Environment Information
description: "You can upload the [Support Package](https://github.com/open-goal/launcher#asking-for-help) provided by the Launcher here, or you can provide the following information: CPU, GPU, OS Version, OpenGOAL Version (found in the window's title bar)"
validations:
required: true
- type: dropdown
attributes:
label: Game Version
options:
- NTSC 1.0 (black label)
- NTSC Greatest Hits version (red label)
- PAL
- JP
validations:
required: true
- type: dropdown
attributes:
label: Have you set the game to something other than `60fps`?
options:
- "No"
- "Yes"
validations:
required: true
+1 -1
View File
@@ -5,4 +5,4 @@ updates:
directory: "/"
schedule:
# Check for updates to GitHub Actions every week
interval: "monthly"
interval: "weekly"
@@ -1,29 +0,0 @@
{
"4000": {
"msg": "Validation Failed: Cannot locate ELF"
},
"4001": {
"msg": "Validation Failed: Game serial missing from valid game database"
},
"4002": {
"msg": "Validation Failed: ELF missing from valid game database"
},
"4010": {
"msg": "Validation Failed: Bad or unexpected ISO contents"
},
"4011": {
"msg": "Validation Failed: Extracted an unexpected amount of files from ISO"
},
"4020": {
"msg": "Validation Failed: ISO Extraction failed, output directory did not contain expected files"
},
"4030": {
"msg": "Decompilation Failed: An error occurred"
},
"4040": {
"msg": "Extraction Failed: Provided game data file was not an ISO file"
},
"4041": {
"msg": "Extraction Failed: Provided game data file was corrupt or has unexpected size"
}
}
@@ -1,35 +0,0 @@
#!/usr/bin/env bash
set -e
DEST=${1}
BIN_SOURCE=${2}
SOURCE=${3}
mkdir -p $DEST
PREP_BIN="${PREP_BIN:-true}"
if [ "$PREP_BIN" = "true" ]; then
cp $BIN_SOURCE/game/gk $DEST
cp $BIN_SOURCE/goalc/goalc $DEST
cp $BIN_SOURCE/decompiler/extractor $DEST
chmod +x $DEST/gk
chmod +x $DEST/goalc
chmod +x $DEST/extractor
fi
mkdir -p $DEST/data
mkdir -p $DEST/data/launcher/
mkdir -p $DEST/data/decompiler/
mkdir -p $DEST/data/game
mkdir -p $DEST/data/log
mkdir -p $DEST/data/game/graphics/opengl_renderer/
cp -r $SOURCE/.github/scripts/releases/error-code-metadata.json $DEST/data/launcher/error-code-metadata.json
cp -r $SOURCE/decompiler/config $DEST/data/decompiler/
cp -r $SOURCE/goal_src $DEST/data
cp -r $SOURCE/game/assets $DEST/data/game/
cp -r $SOURCE/game/graphics/opengl_renderer/shaders $DEST/data/game/graphics/opengl_renderer
cp -r $SOURCE/custom_assets $DEST/data
@@ -1,27 +0,0 @@
#!/usr/bin/env bash
set -e
DEST=${1}
BIN_SOURCE=${2}
SOURCE=${3}
mkdir -p $DEST
cp $BIN_SOURCE/gk.exe $DEST
cp $BIN_SOURCE/goalc.exe $DEST
cp $BIN_SOURCE/extractor.exe $DEST
mkdir -p $DEST/data
mkdir -p $DEST/data/launcher/
mkdir -p $DEST/data/decompiler/
mkdir -p $DEST/data/game
mkdir -p $DEST/data/log
mkdir -p $DEST/data/game/graphics/opengl_renderer/
cp -r $SOURCE/.github/scripts/releases/error-code-metadata.json $DEST/data/launcher/error-code-metadata.json
cp -r $SOURCE/decompiler/config $DEST/data/decompiler/
cp -r $SOURCE/goal_src $DEST/data
cp -r $SOURCE/game/assets $DEST/data/game/
cp -r $SOURCE/game/graphics/opengl_renderer/shaders $DEST/data/game/graphics/opengl_renderer
cp -r $SOURCE/custom_assets $DEST/data
@@ -1,15 +0,0 @@
# Release Process
```mermaid
sequenceDiagram
Project Repo->>Project Repo: New tag created via workflow dispatch
Project Repo->>Project Repo: Create a draft release
Project Repo->>Project Repo: Run Builds
loop Every build we intend to release
Project Repo->>Project Repo: Upload Asset to Draft Release
opt If All assets have been uploaded
Project Repo->>Project Repo: Publish the release
end
end
Launcher Repo->>Project Repo: Pull down latest/pinned release for it's process
```
+45
View File
@@ -0,0 +1,45 @@
name: Re-build Doc Site
# Controls when the action will run. Triggers the workflow on push or pull request
# events but only for the master branch
on:
push:
branches:
- master
paths:
- 'goal_src/**'
- 'docs/gh-pages-proj/**'
jobs:
lint:
name: Update Documentation Site
runs-on: ubuntu-20.04
# Set some sort of timeout in the event of run-away builds. We are limited on concurrent jobs so, get rid of them.
timeout-minutes: 10
steps:
- name: Checkout Repository
uses: actions/checkout@v2
# just do a sed for now
- name: Update Line Count
run: |
chmod +x ./.github/workflows/scripts/line-count.sh
./.github/workflows/scripts/line-count.sh
- name: Update Site
run: |
cd docs/gh-pages-proj
npm install
npm run build
cd ../..
rm -r ./docs/css
rm -r ./docs/img
rm -r ./docs/js
cp -r ./docs/gh-pages-proj/dist/* ./docs/
- name: Add & Commit
uses: EndBug/[email protected]
with:
default_author: github_actions
pull_strategy: "NO-PULL"
message: "Updated github pages site"
-62
View File
@@ -1,62 +0,0 @@
name: Build
on:
push:
branches:
- master
pull_request:
branches:
- master
merge_group: {}
jobs:
# Windows
build_windows_clang:
name: "🖥️ Windows"
uses: ./.github/workflows/windows-build-clang.yaml
with:
cmakePreset: "Release-windows-clang"
cachePrefix: ""
secrets: inherit
build_windows_msvc:
name: "🖥️ Windows"
uses: ./.github/workflows/windows-build-msvc.yaml
with:
cmakePreset: "Release-windows-msvc"
cachePrefix: ""
secrets: inherit
# Linux
build_linux_clang:
name: "🐧 Linux"
uses: ./.github/workflows/linux-build-clang.yaml
with:
cmakePreset: "Release-linux-clang-asan"
cachePrefix: ""
secrets: inherit
build_linux_gcc:
name: "🐧 Linux"
uses: ./.github/workflows/linux-build-gcc.yaml
with:
cmakePreset: "Release-linux-gcc"
cachePrefix: ""
secrets: inherit
# MacOS
build_macos_intel:
name: "🍎 MacOS"
uses: ./.github/workflows/macos-build.yaml
with:
cmakePreset: "Release-macos-clang"
cachePrefix: ""
# Q4 2023 there will hopefully be native arm64 runners
# https://github.com/github/roadmap/issues/528
# build_macos_arm:
# name: "🍎 MacOS"
# uses: ./.github/workflows/macos-build-arm.yaml
# with:
# cmakePreset: "Release-macos-clang"
# cachePrefix: ""
-35
View File
@@ -1,35 +0,0 @@
name: 🏭 Draft Release
on:
workflow_dispatch:
inputs:
bump:
description: 'Semver Bump Type'
required: true
default: 'patch'
type: choice
options:
- patch
- minor
- major
jobs:
cut-release:
runs-on: ubuntu-latest
steps:
# Docs - https://github.com/mathieudutour/github-tag-action
# Workflows cannot trigger other workflows implicitly
# - https://github.community/t/github-actions-workflow-not-triggering-with-tag-push/17053/7
- name: Bump Version and Push Tag
if: github.repository == 'open-goal/jak-project'
id: tag_version
uses: mathieudutour/[email protected]
with:
github_token: ${{ secrets.BOT_PAT }}
tag_prefix: v
default_bump: ${{ github.event.inputs.bump }}
- name: Create Release
env:
GITHUB_TOKEN: ${{ secrets.BOT_PAT }}
run: gh release create ${{ steps.tag_version.outputs.new_tag }} --generate-notes --draft --repo open-goal/jak-project
-87
View File
@@ -1,87 +0,0 @@
name: Inform Pages Repo
on:
push:
branches:
- master
paths:
- 'goal_src/**'
workflow_dispatch: {}
jobs:
gen-docs:
name: Generate Documentation
if: github.repository == 'open-goal/jak-project'
runs-on: ubuntu-20.04
timeout-minutes: 45
env: # overrides: https://github.com/mbitsnbites/buildcache/blob/master/doc/configuration.md
BUILDCACHE_MAX_CACHE_SIZE: 1000000000 # 1gb
BUILDCACHE_COMPRESS_FORMAT: ZSTD
BUILDCACHE_COMPRESS_LEVEL: 19
BUILDCACHE_DIRECT_MODE: true
BUILDCACHE_LOG_FILE: ${{ github.workspace }}/buildcache.log
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install Package Dependencies
run: |
sudo apt update
sudo apt install build-essential cmake \
clang gcc g++ lcov make nasm libxrandr-dev \
libxinerama-dev libxcursor-dev libpulse-dev \
libxi-dev zip ninja-build libgl1-mesa-dev
- name: Setup Buildcache
uses: mikehardy/[email protected]
with:
cache_key: linux-ubuntu-20.04--Release-linux-clang
buildcache_tag: v0.28.1
- name: CMake Generation
env:
CC: clang
CXX: clang++
run: |
cmake -B build --preset=Release-linux-clang \
-DCMAKE_C_COMPILER_LAUNCHER=${{ github.workspace }}/buildcache/bin/buildcache \
-DCMAKE_CXX_COMPILER_LAUNCHER=${{ github.workspace }}/buildcache/bin/buildcache
- name: Build Project
run: cmake --build build --parallel $((`nproc`)) --target goalc
- name: Generate Docs For Jak 1
run: |
./build/goalc/goalc --cmd "(begin (make-group \"all-code\") (gen-docs \"${PWD}\"))" --game jak1
- name: Generate Docs For Jak 2
run: |
./build/goalc/goalc --cmd "(begin (make-group \"all-code\") (gen-docs \"${PWD}\"))" --game jak2
- name: Generate Docs For Jak 3
run: |
./build/goalc/goalc --cmd "(begin (make-group \"all-code\") (gen-docs \"${PWD}\"))" --game jak3
- name: Append File Docstrings
run: |
python ./scripts/ci/docs/append-file-docs.py
- name: Upload Docs Artifact
uses: actions/upload-artifact@v4
with:
name: opengoal-docs
if-no-files-found: error
path: |
./jak1*.json
./jak2*.json
- name: Log Run ID
run: echo ${{ github.run_id }}
- name: Send Dispatch
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.BOT_PAT }}
repository: 'open-goal/open-goal.github.io'
event-type: updateProgress
client-payload: '{"docGenRunId": "${{ github.run_id }}"}'
+29
View File
@@ -0,0 +1,29 @@
name: Linter
# Controls when the action will run. Triggers the workflow on push or pull request
# events but only for the master branch
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
lint:
name: Linting & Formatting
runs-on: ubuntu-20.04
# Set some sort of timeout in the event of run-away builds. We are limited on concurrent jobs so, get rid of them.
timeout-minutes: 10
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Get Package Dependencies
run: sudo apt install clang-format clang-tidy
- name: Check Clang-Formatting
run: |
chmod +x ./third-party/run-clang-format/run-clang-format.py
./third-party/run-clang-format/run-clang-format.py -r common decompiler game goalc test --color always
-72
View File
@@ -1,72 +0,0 @@
name: Lint
on:
push:
branches:
- master
pull_request:
branches:
- master
merge_group: {}
jobs:
formatting:
name: 📝 Formatting
runs-on: ubuntu-20.04
timeout-minutes: 10
steps:
- name: Checkout Repository
uses: actions/checkout@v4
# wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add -
# sudo add-apt-repository 'deb http://apt.llvm.org/focal/ llvm-toolchain-focal-15 main'
# sudo apt install clang-format-15
# --clang-format-executable $(which clang-format-15)
- name: Get Package Dependencies
run: |
sudo apt update
sudo apt install clang-format
clang-format -version
- name: Check Clang-Formatting
run: |
chmod +x ./third-party/run-clang-format/run-clang-format.py
./third-party/run-clang-format/run-clang-format.py -r common decompiler game goalc test tools lsp --color always
required-checks:
name: 📝 Required Checks
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Check for Trailing Whitespace in goal_src
run: python ./scripts/ci/lint-trailing-whitespace.py
- name: Check for Unresolved Conflicts
run: python ./scripts/gsrc/check-for-conflicts.py
- name: Check for Incorrect Asserts
run: python ./scripts/ci/check-for-asserts.py
- name: Check for Invalid Translation Characters
run: python ./scripts/ci/lint-characters.py
optional-checks:
name: 📝 Optional Checks
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install Python Dependencies
run: pip install unidiff colorama
- name: Check for Removed goal_src/ Code
# FYI - if you run this `git diff` on windows, it creates a utf-16 LE file
run: |
git fetch origin
git diff origin/master > lint-changes.diff
python ./scripts/ci/lint-gsrc-removals.py
-78
View File
@@ -1,78 +0,0 @@
name: Linux Build Clang
on:
workflow_call:
inputs:
cmakePreset:
required: true
type: string
cachePrefix:
required: true
type: string
uploadArtifacts:
required: false
type: boolean
default: false
jobs:
build:
name: Clang
runs-on: ubuntu-20.04
timeout-minutes: 60
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install Package Dependencies
run: |
sudo apt update
sudo apt install build-essential cmake \
clang gcc g++ lcov make nasm libxrandr-dev \
libxinerama-dev libxcursor-dev libpulse-dev \
libxi-dev zip ninja-build libgl1-mesa-dev libssl-dev
- name: Setup sccache
uses: hendrikmuhs/[email protected]
with:
variant: sccache
key: linux-ubuntu-20.04-${{ inputs.cachePrefix }}-${{ inputs.cmakePreset }}-${{ github.sha }}
restore-keys: linux-ubuntu-20.04-${{ inputs.cachePrefix }}-${{ inputs.cmakePreset }}
max-size: 1000M
- name: CMake Generation
env:
CC: clang
CXX: clang++
run: |
cmake -B build --preset=${{ inputs.cmakePreset }} \
-DCMAKE_C_COMPILER_LAUNCHER=sccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=sccache
- name: Build Project
run: cmake --build build --parallel $((`nproc`))
- name: Run Tests
env:
GTEST_OUTPUT: "xml:opengoal-test-report.xml"
run: ./test.sh
- name: Prepare artifacts
if: ${{ inputs.uploadArtifacts }}
run: |
strip ./build/goalc/goalc
strip ./build/decompiler/extractor
strip ./build/game/gk
strip ./build/lsp/lsp
- name: Upload artifact
uses: actions/upload-artifact@v4
if: ${{ inputs.uploadArtifacts }}
with:
name: opengoal-linux-${{ inputs.cachePrefix }}
if-no-files-found: error
path: |
./build/goalc/goalc
./build/decompiler/extractor
./build/game/gk
./build/lsp/lsp
-58
View File
@@ -1,58 +0,0 @@
name: Linux Build GCC
on:
workflow_call:
inputs:
cmakePreset:
required: true
type: string
cachePrefix:
required: true
type: string
jobs:
build:
name: GCC
runs-on: ubuntu-20.04
timeout-minutes: 60
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install Package Dependencies
run: |
sudo apt update
sudo apt install build-essential cmake \
clang gcc-10 g++-10 lcov make nasm libxrandr-dev \
libxinerama-dev libxcursor-dev libpulse-dev \
libxi-dev zip ninja-build libgl1-mesa-dev libssl-dev
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 100
sudo update-alternatives --set gcc /usr/bin/gcc-10
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-10 100
sudo update-alternatives --set g++ /usr/bin/g++-10
- name: Setup sccache
uses: hendrikmuhs/[email protected]
with:
variant: sccache
key: linux-ubuntu-20.04-${{ inputs.cachePrefix }}-${{ inputs.cmakePreset }}-${{ github.sha }}
restore-keys: linux-ubuntu-20.04-${{ inputs.cachePrefix }}-${{ inputs.cmakePreset }}
max-size: 1000M
- name: CMake Generation
env:
CC: gcc
CXX: g++
run: |
cmake -B build --preset=${{ inputs.cmakePreset }} \
-DCODE_COVERAGE=OFF \
-DCMAKE_C_COMPILER_LAUNCHER=sccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=sccache
- name: Build Project
run: cmake --build build --parallel $((`nproc`)) -- -w dupbuild=warn
- name: Run Tests
run: ./test.sh
+132
View File
@@ -0,0 +1,132 @@
name: Linux
# Controls when the action will run. Triggers the workflow on push or pull request
# events but only for the master branch
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
build:
strategy:
# Prevent one build from failing everything (although maybe those should be included as experimental builds instead)
fail-fast: false
matrix:
os: [ubuntu-20.04]
config: [Debug] # TODO - Eventually we need to make a Release Config
compiler: [clang, gcc]
experimental: [false]
name: ${{ matrix.config }}-${{ matrix.compiler }}
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.experimental }}
# Set some sort of timeout in the event of run-away builds. We are limited on concurrent jobs so, get rid of them.
timeout-minutes: 20
steps:
# NOTE - useful for debugging
# - name: Dump GitHub context
# env:
# GITHUB_CONTEXT: ${{ toJson(github) }}
# run: |
# echo "$GITHUB_CONTEXT"
- name: Checkout Repository
uses: actions/checkout@v2
- name: Cache Submodules
id: cache-submodules
uses: actions/cache@v2
with:
key: submodules-${{ hashFiles('./.gitmodules') }}
path: |
./third-party/googletest
./third-party/zydis
./third-party/glfw
./.git/modules/
- name: Checkout Submodules
if: steps.cache-submodules.outputs.cache-hit != 'true'
run: git submodule update --init --recursive --jobs 2
- name: Prepare Artifact Git Info
id: git-vars
shell: bash
run: |
echo "##[set-output name=branch;]${GITHUB_REF#refs/heads/}"
ARTIFACT_NAME="commit-$(git rev-parse --short "$GITHUB_SHA")"
if [ ${{ github.event_name == 'pull_request' }} ]; then
echo "##[set-output name=short-sha;]$(git rev-parse --short "${{ github.event.pull_request.head.sha }}")"
if [ ! -z "${{ github.event.pull_request.number }}" ]; then
ARTIFACT_NAME="pr-${{ github.event.pull_request.number }}-commit-$(git rev-parse --short "${{ github.event.pull_request.head.sha }}")"
fi
else
echo "##[set-output name=short-sha;]$(git rev-parse --short "$GITHUB_SHA")"
fi
echo "##[set-output name=artifact-metadata;]${ARTIFACT_NAME}"
- name: Get Package Dependencies
run: sudo apt install build-essential cmake ccache clang gcc g++ lcov make nasm libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev
# # -- SETUP CCACHE - https://cristianadam.eu/20200113/speeding-up-c-plus-plus-github-actions-using-ccache/
# - name: Prepare ccache timestamp
# id: ccache_cache_timestamp
# shell: cmake -P {0}
# run: |
# string(TIMESTAMP current_date "%Y-%m-%d-%H;%M;%S" UTC)
# message("::set-output name=timestamp::${current_date}")
# - name: ccache cache files
# uses: actions/cache@v2
# with:
# path: .ccache
# key: ${{ matrix.config }}-ccache-${{ steps.ccache_cache_timestamp.outputs.timestamp }}
# restore-keys: |
# ${{ matrix.config }}-ccache-
- name: CMake Generation
# run: cmake -D CMAKE_C_COMPILER_LAUNCHER=ccache -D CMAKE_CXX_COMPILER_LAUNCHER=ccache -B build -DCODE_COVERAGE=ON
run: |
if [ "${{ matrix.compiler }}" == 'clang' ]; then
export CC=clang
export CXX=clang++
cmake -B build -DCODE_COVERAGE=ON -DASAN_BUILD=ON
else
export CC=gcc
export CXX=g++
cmake -B build -DCODE_COVERAGE=ON
fi
- name: Build Project
working-directory: ./build
# env:
# CCACHE_BASEDIR: ${GITHUB_WORKSPACE}
# CCACHE_DIR: ${GITHUB_WORKSPACE}/.ccache
# CCACHE_COMPRESS: "true"
# CCACHE_COMPRESSLEVEL: "6"
# CCACHE_MAXSIZE: "400M"
# ccache -p
# ccache -z
# make -j4
# ccache -s
run: make -j4
- name: Run Tests
run: |
if [ "${{ matrix.compiler }}" == 'clang' ]; then
./test.sh
else
./test_code_coverage.sh
fi
- name: Coveralls
if: ${{ matrix.compiler }} != 'clang'
uses: coverallsapp/github-action@master
continue-on-error: true # Sometimes Coveralls has intermittent problems, and codecoverage isn't critical to our success
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
path-to-lcov: ./build/goalc-test_coverage.info
-61
View File
@@ -1,61 +0,0 @@
name: MacOS Build
on:
workflow_call:
inputs:
cmakePreset:
required: true
type: string
cachePrefix:
required: true
type: string
jobs:
build:
name: ARM
runs-on: macos-12
timeout-minutes: 120
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set up ARM64 environment
run: sudo softwareupdate --install-rosetta --agree-to-license
- name: Install Package Dependencies
run: arch -arm64 brew install cmake ninja
- name: Setup sccache
uses: hendrikmuhs/[email protected]
with:
variant: sccache
key: macos-12-${{ inputs.cachePrefix }}-${{ inputs.cmakePreset }}-${{ github.sha }}
restore-keys: macos-12-${{ inputs.cachePrefix }}-${{ inputs.cmakePreset }}
max-size: 1000M
- name: CMake Generation
env:
CC: clang
CXX: clang++
run: |
cmake -B build --preset=${{ inputs.cmakePreset }} \
-DCMAKE_C_COMPILER_LAUNCHER=sccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=sccache
- name: Build Project
run: cmake --build build --parallel $((`sysctl -n hw.logicalcpu`))
- name: Run Tests
run: ./test.sh
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: opengoal-macos-${{ inputs.cachePrefix }}
if-no-files-found: error
path: |
./build/goalc/goalc
./build/decompiler/extractor
./build/game/gk
./build/lsp/lsp
-71
View File
@@ -1,71 +0,0 @@
name: MacOS Build
on:
workflow_call:
inputs:
cmakePreset:
required: true
type: string
cachePrefix:
required: true
type: string
uploadArtifacts:
required: false
type: boolean
default: false
jobs:
build:
name: Intel
runs-on: macos-12
timeout-minutes: 120
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install Package Dependencies
run: brew install cmake nasm ninja
- name: Setup sccache
uses: hendrikmuhs/[email protected]
with:
variant: sccache
key: macos-12-${{ inputs.cachePrefix }}-${{ inputs.cmakePreset }}-${{ github.sha }}
restore-keys: macos-12-${{ inputs.cachePrefix }}-${{ inputs.cmakePreset }}
max-size: 1000M
- name: CMake Generation
env:
CC: clang
CXX: clang++
run: |
cmake -B build --preset=${{ inputs.cmakePreset }} \
-DCMAKE_C_COMPILER_LAUNCHER=sccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=sccache
- name: Build Project
run: cmake --build build --parallel $((`sysctl -n hw.logicalcpu`))
- name: Run Tests
run: ./test.sh
- name: Prepare artifacts
if: ${{ inputs.uploadArtifacts }}
run: |
strip ./build/goalc/goalc
strip ./build/decompiler/extractor
strip ./build/game/gk
strip ./build/lsp/lsp
- name: Upload artifact
uses: actions/upload-artifact@v4
if: ${{ inputs.uploadArtifacts }}
with:
name: opengoal-macos-${{ inputs.cachePrefix }}
if-no-files-found: error
path: |
./build/goalc/goalc
./build/decompiler/extractor
./build/game/gk
./build/lsp/lsp
-104
View File
@@ -1,104 +0,0 @@
name: 🏭 Release Pipeline
on:
push:
tags:
- v*
jobs:
# Windows
build_windows_clang:
name: "🖥️ Windows"
uses: ./.github/workflows/windows-build-clang.yaml
with:
cmakePreset: "Release-windows-clang-static"
cachePrefix: "static"
uploadArtifacts: true
secrets: inherit
# Linux
build_linux_clang:
name: "🐧 Linux"
uses: ./.github/workflows/linux-build-clang.yaml
with:
cmakePreset: "Release-linux-clang-static"
cachePrefix: "static"
uploadArtifacts: true
secrets: inherit
# macOS
build_macos_intel:
name: "🍎 MacOS"
uses: ./.github/workflows/macos-build.yaml
with:
cmakePreset: "Release-macos-clang-static"
cachePrefix: "static"
uploadArtifacts: true
secrets: inherit
# Upload the Artifacts
upload_artifacts:
if: github.repository == 'open-goal/jak-project'
needs:
- build_windows_clang
- build_linux_clang
- build_macos_intel
name: "Upload Artifacts"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Prepare Artifact Folder
run: mkdir -p ./ci-artifacts/final
- uses: actions/download-artifact@v4
name: Download all Artifacts
with:
path: ./ci-artifacts/
- name: Display structure of downloaded files
run: ls -Rl ./ci-artifacts/
- name: Prepare Linux Release Assets
run: |
mkdir -p ./ci-artifacts/linux
./.github/scripts/releases/extract_build_unix.sh ./ci-artifacts/linux ./ci-artifacts/opengoal-linux-static ./
pushd ci-artifacts/linux
TAG_VAL=$(echo ${{ github.REF }} | awk -F'refs/tags/' '{print $2}')
tar czf ../final/opengoal-linux-${TAG_VAL}.tar.gz .
popd
chmod +x ./ci-artifacts/opengoal-linux-static/lsp/lsp
cp ./ci-artifacts/opengoal-linux-static/lsp/lsp ./ci-artifacts/final/opengoal-lsp-linux-${TAG_VAL}.bin
- name: Prepare Windows Build Assets
run: |
mkdir -p ./ci-artifacts/windows
./.github/scripts/releases/extract_build_windows.sh ./ci-artifacts/windows ./ci-artifacts/opengoal-windows-static ./
TAG_VAL=$(echo ${{ github.REF }} | awk -F'refs/tags/' '{print $2}')
7z a -tzip ./ci-artifacts/final/opengoal-windows-${TAG_VAL}.zip ./ci-artifacts/windows/*
cp ./ci-artifacts/opengoal-windows-static/lsp.exe ./ci-artifacts/final/opengoal-lsp-windows-${TAG_VAL}.exe
- name: Prepare macOS Build Assets
run: |
mkdir -p ./ci-artifacts/macos
./.github/scripts/releases/extract_build_unix.sh ./ci-artifacts/macos ./ci-artifacts/opengoal-macos-static ./
pushd ci-artifacts/macos
TAG_VAL=$(echo ${{ github.REF }} | awk -F'refs/tags/' '{print $2}')
tar czf ../final/opengoal-macos-intel-${TAG_VAL}.tar.gz .
popd
chmod +x ./ci-artifacts/opengoal-macos-static/lsp/lsp
cp ./ci-artifacts/opengoal-macos-static/lsp/lsp ./ci-artifacts/final/opengoal-lsp-macos-intel-${TAG_VAL}.bin
- name: Upload Assets
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG_VAL=$(echo ${{ github.REF }} | awk -F'refs/tags/' '{print $2}')
gh release upload "${TAG_VAL}" ${{ github.WORKSPACE }}/ci-artifacts/final/* --repo open-goal/jak-project --clobber
- name: Publish Release
env:
GITHUB_TOKEN: ${{ secrets.BOT_PAT }}
run: |
TAG_VAL=$(echo ${{ github.REF }} | awk -F'refs/tags/' '{print $2}')
gh release edit ${TAG_VAL} --draft=false --repo open-goal/jak-project
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
LINE_COUNT=$(find ./goal_src -name '*.gc' | xargs wc -l | tail -1 | awk -F'[^0-9]+' '{ print $2 }')
sed -i "s/.*value.*/ \"value\": ${LINE_COUNT},/g" ./docs/gh-pages-proj/src/config/progress.json
@@ -1,31 +0,0 @@
name: Update Controller Database
on:
schedule:
- cron: "0 16 * * 1" # every monday @ 12pm EST - https://crontab.guru/#0_16_*_*_1
workflow_dispatch: {}
jobs:
update-controller-db:
if: github.repository == 'open-goal/jak-project'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.BOT_PAT }}
- name: Get Latest DB
run: |
wget -O ./game/assets/sdl_controller_db.txt https://raw.githubusercontent.com/gabomdq/SDL_GameControllerDB/master/gamecontrollerdb.txt
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.BOT_PAT }}
author: 'OpenGOALBot <[email protected]>'
committer: 'OpenGOALBot <[email protected]>'
branch: 'bot/update-controller-db'
base: "master"
body: "Updating Controller Database"
commit-message: "ci: updated controller database"
title: "CI: Periodic Controller Database Update"
@@ -1,75 +0,0 @@
name: Windows Build Clang
on:
workflow_call:
inputs:
cmakePreset:
required: true
type: string
cachePrefix:
required: true
type: string
uploadArtifacts:
required: false
type: boolean
default: false
jobs:
build:
name: Clang
runs-on: windows-2022
# Set some sort of timeout in the event of run-away builds. We are limited on concurrent jobs so, get rid of them.
timeout-minutes: 60
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install NASM
# TODO - Simplify this with just the first command once choco 2.0 rolls out everywhere
run: |
$chocoVersion = choco --version
if ($chocoVersion.StartsWith("2.")) {
choco install nasm --source="${{ github.workspace }}/third-party/nasm"
} else {
choco install ${{ github.workspace }}/third-party/nasm/old/nasm.2.15.05.nupkg
}
- name: Setup sccache
uses: hendrikmuhs/[email protected]
with:
variant: sccache
key: windows-2022-${{ inputs.cachePrefix }}-${{ inputs.cmakePreset }}-${{ github.sha }}
restore-keys: windows-2022-${{ inputs.cachePrefix }}-${{ inputs.cmakePreset }}
max-size: 1000M
- uses: ilammy/msvc-dev-cmd@v1
# https://github.com/actions/runner-images/issues/5459
# Causes binaries to be built differently, get rid of this garbage
- name: Remove Strawberry Perl from PATH
run: |
$env:PATH = $env:PATH -replace "C:\\Strawberry\\c\\bin;", ""
"PATH=$env:PATH" | Out-File -FilePath $env:GITHUB_ENV -Append
- name: CMake Generation
shell: cmd
run: cmake -B build --preset=${{ inputs.cmakePreset }} -DCMAKE_C_COMPILER_LAUNCHER=sccache -DCMAKE_CXX_COMPILER_LAUNCHER=sccache .
- name: Build Project
shell: cmd
run: cmake --build build --parallel %NUMBER_OF_PROCESSORS%
- name: Run Tests
env:
GTEST_OUTPUT: "xml:opengoal-test-report.xml"
run: ./build/bin/goalc-test.exe --gtest_color=yes --gtest_brief=0 --gtest_filter="-*MANUAL_TEST*"
- name: Upload artifact
uses: actions/upload-artifact@v4
if: ${{ inputs.uploadArtifacts }}
with:
name: opengoal-windows-${{ inputs.cachePrefix }}
if-no-files-found: error
path: ./build/bin
-65
View File
@@ -1,65 +0,0 @@
name: Windows Build MSVC
on:
workflow_call:
inputs:
cmakePreset:
required: true
type: string
cachePrefix:
required: true
type: string
jobs:
build:
name: MSVC
runs-on: windows-2022
timeout-minutes: 60
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install NASM
# TODO - Simplify this with just the first command once choco 2.0 rolls out everywhere
run: |
$chocoVersion = choco --version
if ($chocoVersion.StartsWith("2.")) {
choco install nasm --source="${{ github.workspace }}/third-party/nasm"
} else {
choco install ${{ github.workspace }}/third-party/nasm/old/nasm.2.15.05.nupkg
}
- name: Setup sccache
uses: hendrikmuhs/[email protected]
with:
variant: sccache
key: windows-2022-${{ inputs.cachePrefix }}-${{ inputs.cmakePreset }}-${{ github.sha }}
restore-keys: windows-2022-${{ inputs.cachePrefix }}-${{ inputs.cmakePreset }}
max-size: 1000M
- uses: ilammy/msvc-dev-cmd@v1
# Fixes an issue with the image causing builds to fail - https://github.com/actions/runner-images/issues/8598
- name: Remove Strawberry Perl from PATH
run: |
$env:PATH = $env:PATH -replace "C:\\Strawberry\\c\\bin;", ""
"PATH=$env:PATH" | Out-File -FilePath $env:GITHUB_ENV -Append
- name: CMake Generation - MSVC
shell: cmd
run: cmake -B build --preset=${{ inputs.cmakePreset }} -DCMAKE_C_COMPILER_LAUNCHER=sccache -DCMAKE_CXX_COMPILER_LAUNCHER=sccache .
- name: Build Project
shell: cmd
run: |
set CL=/MP
cmake --build build --parallel %NUMBER_OF_PROCESSORS%
- name: Run Tests
timeout-minutes: 10
env:
GTEST_OUTPUT: "xml:opengoal-test-report.xml"
run: |
./build/bin/goalc-test.exe --gtest_color=yes --gtest_brief=0 --gtest_filter="-*MANUAL_TEST*"
+94
View File
@@ -0,0 +1,94 @@
name: Windows
# Controls when the action will run. Triggers the workflow on push or pull request
# events but only for the master branch
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
build:
strategy:
# Prevent one build from failing everything (although maybe those should be included as experimental builds instead)
fail-fast: false
matrix:
os: [windows-2019]
config: [Release]
experimental: [false]
name: ${{ matrix.config }}
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.experimental }}
# Set some sort of timeout in the event of run-away builds. We are limited on concurrent jobs so, get rid of them.
timeout-minutes: 20
steps:
# NOTE - useful for debugging
# - name: Dump GitHub context
# env:
# GITHUB_CONTEXT: ${{ toJson(github) }}
# run: |
# echo "$GITHUB_CONTEXT"
- name: Checkout Repository
uses: actions/checkout@v2
- name: Cache Submodules
id: cache-submodules
uses: actions/cache@v2
with:
key: submodules-${{ hashFiles('./.gitmodules') }}
path: |
./third-party/googletest
./third-party/zydis
./third-party/glfw
./.git/modules/
- name: Checkout Submodules
if: steps.cache-submodules.outputs.cache-hit != 'true'
run: git submodule update --init --recursive --jobs 2
- name: Prepare Artifact Git Info
shell: bash
run: |
echo "##[set-output name=branch;]${GITHUB_REF#refs/heads/}"
ARTIFACT_NAME="commit-$(git rev-parse --short "$GITHUB_SHA")"
if [ ${{ github.event_name == 'pull_request' }} ]; then
echo "##[set-output name=short-sha;]$(git rev-parse --short "${{ github.event.pull_request.head.sha }}")"
if [ ! -z "${{ github.event.pull_request.number }}" ]; then
ARTIFACT_NAME="pr-${{ github.event.pull_request.number }}-commit-$(git rev-parse --short "${{ github.event.pull_request.head.sha }}")"
fi
else
echo "##[set-output name=short-sha;]$(git rev-parse --short "$GITHUB_SHA")"
fi
echo "##[set-output name=artifact-metadata;]${ARTIFACT_NAME}"
id: git-vars
# https://github.com/ilammy/setup-nasm
- uses: ilammy/setup-nasm@v1
- name: CMake Generation
shell: cmd # ideally id like everything to be powershell but running this bat file is finicky
run: |
call "C:/Program Files (x86)/Microsoft Visual Studio/2019/Enterprise/VC/Auxiliary/Build/vcvars64.bat"
cmake --version
cmake -B build -DCMAKE_BUILD_TYPE=Release -G "NMake Makefiles" .
- name: Build Project
working-directory: ./build
shell: cmd
run: |
call "C:/Program Files (x86)/Microsoft Visual Studio/2019/Enterprise/VC/Auxiliary/Build/vcvars64.bat"
set CL=/MP
nmake
- name: Run Tests
timeout-minutes: 5
run: |
$env:NEXT_DIR = $pwd
$env:FAKE_ISO_PATH = "/game/fake_iso.txt"
./build/bin/goalc-test.exe --gtest_color=yes
+2 -64
View File
@@ -1,25 +1,13 @@
# logs
/log
prof.json
# for CMake
/Testing
# for clion
cmake-build-debug/*
cmake-build-debug--o0/*
.idea/*
build/*
/decompiler_out*
decompiler_out/*
logs/*
profile_data/*
# for vscode/clangd
.cache/*
.DS_Store
# wsl apparently builds to here?
linux-default/
# for Nix
/result*
@@ -28,59 +16,9 @@ linux-default/
.#*#
*.pyc
# output from our tools
*.bin
*.log
*.p2s
savestate-out/
savestate_out/
failures/
ee-results*.json
search-results.json
.env
/search-results.json
# graphics debug
debug_out/
glb_out/
gfx_dumps/
screenshots/
# game stuff
game_config/*
imgui.ini
# website stuff
node_modules/
# texture replacements
custom_assets/jak1/texture_replacements/*
custom_assets/jak2/texture_replacements/*
custom_assets/jak3/texture_replacements/*
# merc replacements
custom_assets/jak1/merc_replacements/*
custom_assets/jak2/merc_replacements/*
custom_assets/jak3/merc_replacements/*
# generated cmake files
svnrev.h
common/versions/revision.h
ci-artifacts/
out/build/
__pycache__/
# sqlite stuff
*.db
*.db-journal
# backup files from OpenMaya
*.bak
# docs
/jak1-*.json
/jak2-*.json
/TODO.md
unifont-15.0.03.ttf
*.diff
goalc-report.html
ee-results.json
+9
View File
@@ -0,0 +1,9 @@
[submodule "third-party/googletest"]
path = third-party/googletest
url = https://github.com/google/googletest.git
[submodule "third-party/zydis"]
path = third-party/zydis
url = https://github.com/zyantific/zydis.git
[submodule "third-party/glfw"]
path = third-party/glfw
url = https://github.com/glfw/glfw
-11
View File
@@ -1,11 +0,0 @@
{
"endOfLine": "auto",
"overrides": [
{
"files": ["**/*.jsonc"],
"options": {
"parser": "json"
}
}
]
}
+1
View File
@@ -0,0 +1 @@
+/goal_src
+66 -391
View File
@@ -1,419 +1,94 @@
{
"version": "0.2.1",
"defaults": {},
"configurations": [
"version" : "0.2.1", "defaults" : {}, "configurations" : [
{
"type": "default",
"project": "CMakeLists.txt",
"projectTarget": "goalc-test.exe (bin\\goalc-test.exe)",
"name": "Tests - Unit-Tests - Summary",
"args": [
"--gtest_brief=1"
]
"type" : "default",
"project" : "CMakeLists.txt",
"projectTarget" : "goalc-test.exe (bin\\goalc-test.exe)",
"name" : "Tests - Unit-Tests - Summary",
"args" : ["--gtest_brief=1"]
},
{
"type": "default",
"project": "CMakeLists.txt",
"projectTarget": "goalc-test.exe (bin\\goalc-test.exe)",
"name": "Tests - Unit-Tests - Verbose",
"args": [
"--gtest_brief=0"
]
"type" : "default",
"project" : "CMakeLists.txt",
"projectTarget" : "goalc-test.exe (bin\\goalc-test.exe)",
"name" : "Tests - Unit-Tests - Verbose",
"args" : ["--gtest_brief=0"]
},
{
"type": "default",
"project": "CMakeLists.txt",
"projectTarget": "goalc-test.exe (bin\\goalc-test.exe)",
"name": "Tests - Draft Tests - Verbose",
"args": [
"--gtest_brief=0",
"--gtest_filter=\"*Draft*\""
]
"type" : "default",
"project" : "CMakeLists.txt",
"projectTarget" : "goalc-test.exe (bin\\goalc-test.exe)",
"name" : "Tests - Draft Tests - Verbose",
"args" : ["--gtest_brief=0", "--gtest_filter=\"*Draft*\""]
},
{
"type": "default",
"project": "CMakeLists.txt",
"projectTarget": "goalc-test.exe (bin\\goalc-test.exe)",
"name": "Tests - TypeConsistency - Verbose",
"args": [
"--gtest_brief=0",
"--gtest_filter=\"*TypeConsistency*\""
]
"type" : "default",
"project" : "CMakeLists.txt",
"projectTarget" : "goalc-test.exe (bin\\goalc-test.exe)",
"name" : "Tests - TypeConsistency - Verbose",
"args" : ["--gtest_brief=0", "--gtest_filter=\"*TypeConsistency*\""]
},
{
"type": "default",
"project": "CMakeLists.txt",
"projectTarget": "goalc-test.exe (bin\\goalc-test.exe)",
"name": "Tests - TypeConsistency - Jak 2 - Verbose",
"args": [
"--gtest_brief=0",
"--gtest_filter=\"*Jak2TypeConsistency*\""
]
"type" : "default",
"project" : "CMakeLists.txt",
"projectTarget" : "offline-test.exe (bin\\offline-test.exe)",
"name" : "Tests - Offline Tests",
"args" : ["${workspaceRoot}/iso_data/jak1"]
},
{
"type": "default",
"project": "CMakeLists.txt",
"projectTarget": "goalc-test.exe (bin\\goalc-test.exe)",
"name": "Tests - WithGameTests - Verbose",
"args": [
"--gtest_brief=0",
"--gtest_filter=\"*WithGameTests*\""
]
"type" : "default",
"project" : "CMakeLists.txt",
"projectTarget" : "gk.exe (bin\\gk.exe)",
"name" : "Run - Runtime (no kernel)",
"args" : [ "-fakeiso", "-debug", "-nokernel", "-v", "-nodisplay" ]
},
{
"type": "default",
"project": "CMakeLists.txt",
"projectTarget": "offline-test.exe (bin\\offline-test.exe)",
"name": "Tests - Offline Tests - Jak 1",
"args": [
"--iso_data_path",
"${workspaceRoot}/iso_data/jak1",
"--game",
"jak1"
]
"type" : "default",
"project" : "CMakeLists.txt",
"projectTarget" : "gk.exe (bin\\gk.exe)",
"name" : "Run - Runtime (with kernel)",
"args" : [ "-fakeiso", "-debug", "-v", "-nodisplay" ]
},
{
"type": "default",
"project": "CMakeLists.txt",
"projectTarget": "offline-test.exe (bin\\offline-test.exe)",
"name": "Tests - Offline Tests - Jak 2",
"args": [
"--iso_data_path",
"${workspaceRoot}/iso_data/jak2",
"--game",
"jak2"
]
"type" : "default",
"project" : "CMakeLists.txt",
"projectTarget" : "goalc.exe (bin\\goalc.exe)",
"name" : "Run - REPL"
},
{
"type": "default",
"project": "CMakeLists.txt",
"projectTarget": "offline-test.exe (bin\\offline-test.exe)",
"name": "Tests - Offline Tests - Jak 3",
"args": [
"--iso_data_path",
"${workspaceRoot}/iso_data/jak3",
"--game",
"jak3"
]
"type" : "default",
"project" : "CMakeLists.txt",
"projectTarget" : "decompiler.exe (bin\\decompiler.exe)",
"name" : "Run - Decompiler - Jak 1",
"args" : [ "${workspaceRoot}/decompiler/config/jak1_ntsc_black_label.jsonc", "${workspaceRoot}/iso_data/jak1", "${workspaceRoot}/decompiler_out/jak1"]
},
{
"type": "default",
"project": "CMakeLists.txt",
"projectTarget": "offline-test.exe (bin\\offline-test.exe)",
"name": "Tests - Offline Tests - Jak 1 - Specific File",
"args": [
"--iso_data_path",
"${workspaceRoot}/iso_data/jak1",
"--game",
"jak1",
"--file",
"euler-h"
]
"type" : "default",
"project" : "CMakeLists.txt",
"projectTarget" : "decompiler.exe (bin\\decompiler.exe)",
"name" : "Run - Disassembler - Jak 1",
"args" : [ "${workspaceRoot}/decompiler/config/jak1_ntsc_black_label.jsonc", "${workspaceRoot}/iso_data/jak1", "${workspaceRoot}/decompiler_out/jak1_ir1"]
},
{
"type": "default",
"project": "CMakeLists.txt",
"projectTarget": "offline-test.exe (bin\\offline-test.exe)",
"name": "Tests - Offline Tests - Jak 2 - Specific File",
"args": [
"--iso_data_path",
"${workspaceRoot}/iso_data/jak2",
"--game",
"jak2",
"--file",
"oracle-texture"
]
"type" : "default",
"project" : "CMakeLists.txt",
"projectTarget" : "decompiler.exe (bin\\decompiler.exe)",
"name" : "Run - Decompiler - Jak 2",
"args" : [ "${workspaceRoot}/decompiler/config/jak2_ntsc_v1.jsonc", "${workspaceRoot}/iso_data/jak2", "${workspaceRoot}/decompiler_out/jak2"]
},
{
"type": "default",
"project": "CMakeLists.txt",
"projectTarget": "gk.exe (bin\\gk.exe)",
"name": "Game - Jak 1 - Runtime",
"args": [
"-v",
"--game",
"jak1",
"--",
"-fakeiso",
"-debug"
]
"type" : "default",
"project" : "CMakeLists.txt",
"projectTarget" : "memory_dump_tool.exe (bin\\memory_dump_tool.exe)",
"name" : "Run - EE Memory Analyze",
"args" : [ "${workspaceRoot}/eeMemory.bin", "${workspaceRoot}"]
},
{
"type": "default",
"project": "CMakeLists.txt",
"projectTarget": "gk.exe (bin\\gk.exe)",
"name": "Game - Jak 1 - Runtime (boot)",
"args": [
"-v",
"--game",
"jak1",
"--",
"-boot",
"-fakeiso",
"-debug"
]
},
{
"type": "default",
"project": "CMakeLists.txt",
"projectTarget": "gk.exe (bin\\gk.exe)",
"name": "Game - Jak 2 - Runtime (no boot)",
"args": [
"-v",
"--game",
"jak2",
"--",
"-fakeiso",
"-debug"
]
},
{
"type": "default",
"project": "CMakeLists.txt",
"projectTarget": "gk.exe (bin\\gk.exe)",
"name": "Game - Jak 2 - Runtime (boot)",
"args": [
"-v",
"--game",
"jak2",
"--",
"-boot",
"-fakeiso",
"-debug"
]
},
{
"type": "default",
"project": "CMakeLists.txt",
"projectTarget": "gk.exe (bin\\gk.exe)",
"name": "Game - Jak 2 - Runtime (release)",
"args": [
"-v",
"--game",
"jak2",
"--",
"-boot",
"-fakeiso"
]
},
{
"type": "default",
"project": "CMakeLists.txt",
"projectTarget": "gk.exe (bin\\gk.exe)",
"name": "Game - Jak 3 - Runtime (boot)",
"args": [
"-v",
"--game",
"jak3",
"--",
"-boot",
"-fakeiso",
"-debug"
]
},
{
"type": "default",
"project": "CMakeLists.txt",
"projectTarget": "goalc.exe (bin\\goalc.exe)",
"name": "REPL - Jak 1",
"args": [
"--user-auto",
"--game",
"jak1"
]
},
{
"type": "default",
"project": "CMakeLists.txt",
"projectTarget": "goalc.exe (bin\\goalc.exe)",
"name": "REPL - Jak 2",
"args": [
"--user-auto",
"--game",
"jak2"
]
},
{
"type": "default",
"project": "CMakeLists.txt",
"projectTarget": "decompiler.exe (bin\\decompiler.exe)",
"name": "Decompiler - Jak 1",
"args": [
"${workspaceRoot}/decompiler/config/jak1/jak1_config.jsonc",
"${workspaceRoot}/iso_data",
"${workspaceRoot}/decompiler_out",
"--version",
"ntsc_v1"
]
},
{
"type": "default",
"project": "CMakeLists.txt",
"projectTarget": "decompiler.exe (bin\\decompiler.exe)",
"name": "Decompiler - Jak 1 NTSC-U v2",
"args": [
"${workspaceRoot}/decompiler/config/jak1/jak1_config.jsonc",
"${workspaceRoot}/iso_data",
"${workspaceRoot}/decompiler_out",
"--version",
"ntsc_v2"
]
},
{
"type": "default",
"project": "CMakeLists.txt",
"projectTarget": "decompiler.exe (bin\\decompiler.exe)",
"name": "Decompiler - Jak 1 PAL",
"args": [
"${workspaceRoot}/decompiler/config/jak1/jak1_config.jsonc",
"${workspaceRoot}/iso_data",
"${workspaceRoot}/decompiler_out",
"--version",
"pal"
]
},
{
"type": "default",
"project": "CMakeLists.txt",
"projectTarget": "decompiler.exe (bin\\decompiler.exe)",
"name": "Decompiler - Jak 2",
"args": [
"${workspaceRoot}/decompiler/config/jak2/jak2_config.jsonc",
"${workspaceRoot}/iso_data",
"${workspaceRoot}/decompiler_out",
"--version",
"ntsc_v1"
]
},
{
"type": "default",
"project": "CMakeLists.txt",
"projectTarget": "decompiler.exe (bin\\decompiler.exe)",
"name": "Decompiler - Jak 2 - Extract",
"args": [
"${workspaceRoot}/decompiler/config/jak2/jak2_config.jsonc",
"${workspaceRoot}/iso_data",
"${workspaceRoot}/decompiler_out",
"--version",
"ntsc_v1",
"--config-override \"{\\\"decompile_code\\\": false, \\\"levels_extract\\\": true, \\\"allowed_objects\\\": []}\""
]
},
{
"type": "default",
"project": "CMakeLists.txt",
"projectTarget": "decompiler.exe (bin\\decompiler.exe)",
"name": "Decompiler - Jak 3",
"args": [
"${workspaceRoot}/decompiler/config/jak3/jak3_config.jsonc",
"${workspaceRoot}/iso_data",
"${workspaceRoot}/decompiler_out",
"--version",
"ntsc_v1"
]
},
{
"type": "default",
"project": "CMakeLists.txt",
"projectTarget": "decompiler.exe (bin\\decompiler.exe)",
"name": "Decompiler - Jak 3 - Extract",
"args": [
"${workspaceRoot}/decompiler/config/jak3/jak3_config.jsonc",
"${workspaceRoot}/iso_data",
"${workspaceRoot}/decompiler_out",
"--version",
"ntsc_v1",
"--config-override \"{\\\"decompile_code\\\": false, \\\"levels_extract\\\": true, \\\"allowed_objects\\\": []}\""
]
},
{
"type": "default",
"project": "CMakeLists.txt",
"projectTarget": "memory_dump_tool.exe (bin\\memory_dump_tool.exe)",
"name": "Tools - EE Memory Analyze - Jak 1",
"args": [
"\"${workspaceRoot}/eeMemory.bin\"",
"--output-path",
"\"${workspaceRoot}\"",
"--game",
"jak1"
]
},
{
"type": "default",
"project": "CMakeLists.txt",
"projectTarget": "memory_dump_tool.exe (bin\\memory_dump_tool.exe)",
"name": "Tools - EE Memory Analyze - Jak 2",
"args": [
"\"${workspaceRoot}/eeMemory.bin\"",
"--output-path",
"\"${workspaceRoot}\"",
"--game",
"jak2"
]
},
{
"type": "default",
"project": "CMakeLists.txt",
"projectTarget": "dgo_unpacker.exe (bin\\dgo_unpacker.exe)",
"name": "Tools - DGO Unpacker (test)",
"args": [
"C:\\GameData\\Jak1\\Backup\\DGO-PAL\\GAME",
"C:\\GameData\\Jak1\\Backup\\DISC-PAL\\CGO\\GAME.CGO"
]
},
{
"type": "default",
"project": "CMakeLists.txt",
"projectTarget": "extractor.exe (bin\\extractor.exe)",
"name": "Tools - Extractor - Full",
"args": [
"\"E:\\ISOs\\Jak\\Jak 1.iso\""
]
},
{
"type": "default",
"project": "CMakeLists.txt",
"projectTarget": "lsp.exe (bin\\lsp.exe)",
"name": "Tools - LSP",
"args": []
},
{
"type": "default",
"project": "CMakeLists.txt",
"projectTarget": "type_searcher.exe (bin\\type_searcher.exe)",
"name": "Tools - Type Searcher",
"args": [
"--game",
"jak2",
"--output-path",
"./search-results.json",
"--size",
255,
"--fields",
"[{\\\"type\\\":\\\"quaternion\\\",\\\"offset\\\":48}]"
]
},
{
"type": "default",
"project": "CMakeLists.txt",
"projectTarget": "formatter.exe (bin\\formatter.exe)",
"name": "Tools - Formatter - Inplace",
"args": [
"--write",
"--file",
"C:\\Users\\xtvas\\Repositories\\opengoal\\jak-project\\goal_src\\jak1\\engine\\camera\\cam-states.gc"
]
},
{
"type": "default",
"project": "CMakeLists.txt",
"projectTarget": "goalc-test.exe (bin\\goalc-test.exe)",
"name": "Tests - Formatter",
"args": [
"--gtest_brief=0",
"--gtest_filter=\"*FormatterTests*\""
]
"type" : "default",
"project" : "CMakeLists.txt",
"projectTarget" : "dgo_unpacker.exe (bin\\dgo_unpacker.exe)",
"name" : "Run - DGO Unpacker (test)",
"args" : [ "C:\\GameData\\Jak1\\Backup\\DGO-PAL\\GAME", "C:\\GameData\\Jak1\\Backup\\DISC-PAL\\CGO\\GAME.CGO"]
}
]
}
}
-9
View File
@@ -1,9 +0,0 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
// List of extensions which should be recommended for users of this workspace.
"recommendations": ["opengoal.opengoal"],
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
"unwantedRecommendations": []
}
+3 -4
View File
@@ -5,13 +5,12 @@
"version": "0.2.0",
"configurations": [
{
"name": "run python script",
"name": "Cleanup All Types",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/scripts/gsrc/compare-compilation-outputs.py",
"program": "${workspaceFolder}/scripts/cleanup-all-types.py",
"console": "integratedTerminal",
"cwd": "${workspaceFolder}",
"args": []
"cwd": "${workspaceFolder}"
},
]
}
-26
View File
@@ -1,26 +0,0 @@
{
"og:ignore-from-loc": {
"scope": "opengoal",
"prefix": ["og:ignore-from-loc"],
"body": [";; og:ignore-from-loc"],
"description": "Marks the file to be excluded from the LoC count"
},
"og:ignore-errors": {
"scope": "opengoal",
"prefix": ["og:ignore-errors"],
"body": [";; og:ignore-errors"],
"description": "Ignore lines beginning with ';; ERROR:' or ';; WARN:' when copying decompiled code into it"
},
"og:ignore-form": {
"scope": "opengoal",
"prefix": ["og:ignore-form"],
"body": [";; og:ignore-form:${1:substr}"],
"description": "If the provided substr is found in the starting line of a form, the entire form will be omitted when copying decompiled code into the file"
},
"og:preserve-this": {
"scope": "opengoal",
"prefix": ["og:preserve-this"],
"body": [";; og:preserve-this"],
"description": "When updating GOAL source files from decomp, CI will pick up if this comment is deleted in the diff. Use this to keep track of manually patched sections of code."
},
}
+253 -9
View File
@@ -1,12 +1,256 @@
{
"[opengoal]": {
"editor.quickSuggestions": {
"other": true,
"comments": true,
"strings": true
},
"editor.wordBasedSuggestions": "matchingDocuments",
"editor.snippetSuggestions": "top"
"files.associations": {
"*ir2.asm": "Plain Text",
".gc": "lisp",
".gs": "lisp",
".gd": "lisp"
},
"cmake.configureOnOpen": false
"highlight.maxMatches": 50000, // Maximum number of matches to decorate per regex, in order not to crash the app with accidental cathastropic regexes
"highlight.regexes": {
"(\\.function.*)": {
"filterFileRegex": ".*ir2\\.asm",
"decorations": [
{
"overviewRulerColor": "transparent",
"color": "#41f041",
"fontWeight": "bold"
}
]
},
"(<uninitialized>)": {
"filterFileRegex": ".*ir2\\.asm",
"decorations": [
{
"overviewRulerColor": "transparent",
"color": "#c72855"
}
]
},
"(INFO:.*)": {
"filterFileRegex": ".*ir2\\.asm",
"decorations": [
{
"overviewRulerColor": "transparent",
"color": "yellow",
"fontWeight": "bold"
}
]
},
"(sp, -?\\d+ )": {
"filterFileRegex": ".*ir2\\.asm",
"decorations": [
{
"overviewRulerColor": "transparent",
"color": "#00ff08",
"fontWeight": "bold"
}
]
},
"(at op \\d+)": {
"filterFileRegex": ".*ir2\\.asm",
"decorations": [
{
"overviewRulerColor": "transparent",
"color": "#4de0ff"
}
]
},
"(.*;; )(\\[\\s*\\d+\\])": {
"filterFileRegex": ".*ir2\\.asm",
"decorations": [
{},
{
"overviewRulerColor": "transparent",
"color": "#4de0ff"
}
]
},
"(lw.+, )(.+\\(s7\\))": {
"filterFileRegex": ".*ir2\\.asm",
"decorations": [
{},
{
"overviewRulerColor": "transparent",
"color": "magenta"
}
]
},
"(lwu t9)": {
"filterFileRegex": ".*ir2\\.asm",
"decorations": [
{
"overviewRulerColor": "transparent",
"color": "magenta"
}
]
},
"(jalr)": {
"filterFileRegex": ".*ir2\\.asm",
"decorations": [
{
"overviewRulerColor": "transparent",
"color": "magenta"
}
]
},
"(L\\d+)": {
"filterFileRegex": ".*ir2\\.asm",
"decorations": [
{
"overviewRulerColor": "transparent",
"color": "#f0d541"
}
]
},
"(B\\d+:)": {
"filterFileRegex": ".*ir2\\.asm",
"decorations": [
{
"overviewRulerColor": "transparent",
"color": "#f07341"
}
]
},
"([\\s\\(])(at)([\\s\\),:-])": {
"filterFileRegex": ".*ir2\\.asm",
"decorations": [
{},
{
"overviewRulerColor": "transparent",
"color": "red"
}
]
},
"([\\s\\(])(gp)([\\s\\),:-])": {
"filterFileRegex": ".*ir2\\.asm",
"decorations": [
{},
{
"overviewRulerColor": "transparent",
"color": "#64c5e3"
}
]
},
"([\\s\\(])(v0)([\\s\\),:-])": {
"filterFileRegex": ".*ir2\\.asm",
"decorations": [
{},
{
"overviewRulerColor": "transparent",
"color": "#c6c1e6"
}
]
},
"([\\s\\(])(v1)([\\s\\),:-])": {
"filterFileRegex": ".*ir2\\.asm",
"decorations": [
{},
{
"overviewRulerColor": "transparent",
"color": "#dae6be"
}
]
},
"([\\s\\(])(s6)([\\s\\),:-])": {
"filterFileRegex": ".*ir2\\.asm",
"decorations": [
{},
{
"overviewRulerColor": "transparent",
"color": "#41f0b0"
}
]
},
"([\\s\\(])(a0)([\\s\\),:-])": {
"filterFileRegex": ".*ir2\\.asm",
"decorations": [
{},
{
"overviewRulerColor": "transparent",
"color": "pink"
}
]
},
"([\\s\\(])(a1)([\\s\\),:-])": {
"filterFileRegex": ".*ir2\\.asm",
"decorations": [
{},
{
"overviewRulerColor": "transparent",
"color": "#e065bb"
}
]
},
"([\\s\\(])(a2)([\\s\\),:-])": {
"filterFileRegex": ".*ir2\\.asm",
"decorations": [
{},
{
"overviewRulerColor": "transparent",
"color": "#6dd1a6"
}
]
},
"([\\s\\(])(a3)([\\s\\),:-])": {
"filterFileRegex": ".*ir2\\.asm",
"decorations": [
{},
{
"overviewRulerColor": "transparent",
"color": "#bad192"
}
]
},
"([\\s\\(])(t0)([\\s\\),:-])": {
"filterFileRegex": ".*ir2\\.asm",
"decorations": [
{},
{
"overviewRulerColor": "transparent",
"color": "#b56b82"
}
]
},
"([\\s\\(])(t1)([\\s\\),:-])": {
"filterFileRegex": ".*ir2\\.asm",
"decorations": [
{},
{
"overviewRulerColor": "transparent",
"color": "#6d32ba"
}
]
},
"([\\s\\(])(t2)([\\s\\),:-])": {
"filterFileRegex": ".*ir2\\.asm",
"decorations": [
{},
{
"overviewRulerColor": "transparent",
"color": "#db9267"
}
]
},
"([\\s\\(])(t3)([\\s\\),:-])": {
"filterFileRegex": ".*ir2\\.asm",
"decorations": [
{},
{
"overviewRulerColor": "transparent",
"color": "#92db67"
}
]
},
"(WARN:.*)": {
"filterFileRegex": ".*ir2\\.asm",
"decorations": [
{
"overviewRulerColor": "red",
"color": "red",
"fontWeight": "bold",
"filterFileRegex": ".*ir2\\.asm"
}
]
}
}
}
+40 -233
View File
@@ -1,79 +1,18 @@
# Top Level CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
set(CMAKE_CXX_STANDARD 20)
project(jak)
include(CTest)
# Include third-party modules
set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/third-party/cmake/modules/)
# Default to Release mode
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE "Release")
set(CMAKE_BUILD_TYPE "Debug")
endif()
# Potentially statically build the project
option(STATICALLY_LINK "Build for release purposes (statically link everything)" OFF)
message(STATUS "Statically Link? ${STATICALLY_LINK}")
if(STATICALLY_LINK)
set(BUILD_SHARED_LIBS OFF)
set(BUILD_STATIC_LIBS ON)
else()
set(BUILD_SHARED_LIBS ON)
set(BUILD_STATIC_LIBS OFF)
endif()
set(CMAKE_CXX_STANDARD 17)
# For clangd
if (EXISTS "${CMAKE_CURRENT_BINARY_DIR}/compile_commands.json" )
configure_file(
"${CMAKE_CURRENT_BINARY_DIR}/compile_commands.json"
"${PROJECT_SOURCE_DIR}/build/compile_commands.json")
endif()
# Setup compiler flags
if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
message(STATUS "Clang Detected - Setting Defaults")
set(CMAKE_CXX_FLAGS
"${CMAKE_CXX_FLAGS} \
-fcxx-exceptions \
-fexceptions \
-fdiagnostics-color=always \
-mavx \
-Wall \
-Wno-c++11-narrowing \
-Wno-c++98-compat \
-O3 \
-D_CRT_SECURE_NO_WARNINGS")
# Increase stack size for windows, who's default is too low
if(WIN32)
# Increase the reserved stack size for all threads to 16MB
# Note: this is only _reserved_ memory, not necessarily _committed_ memory
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${LDFLAGS} -Xlinker /STACK:16000000")
else()
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -ggdb -g -Wextra")
endif()
# additional c++ and linker flags for release mode for our projects
if(CMAKE_BUILD_TYPE MATCHES "Release" OR CMAKE_BUILD_TYPE MATCHES "RelWithDebInfo")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3")
endif()
if(CMAKE_BUILD_TYPE MATCHES "RelWithDebInfo")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${LDFLAGS} -Xlinker /debug")
endif()
# Fuzzing
if(NOT STATICALLY_LINK AND ASAN_BUILD)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -O1")
message(STATUS "Doing ASAN build")
endif()
# Warnings
set(THIRDPARTY_IGNORED_WARNINGS "-Wno-everything")
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
message(STATUS "GCC detected - Setting Defaults")
# Set default compile flags for GCC
# optimization level can be set here. Note that game/ overwrites this for building game C++ code.
if(UNIX)
message(STATUS "GCC detected, adding compile flags")
set(CMAKE_CXX_FLAGS
"${CMAKE_CXX_FLAGS} \
-Wall \
@@ -84,74 +23,16 @@ elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
-Wcast-qual \
-Wdisabled-optimization \
-Wformat \
-Wextra \
-Wmissing-include-dirs \
-Woverloaded-virtual \
-Wredundant-decls \
-Wshadow \
-Wsign-promo \
-fdiagnostics-color=always \
-mavx")
# additional c++ flags for release mode for our projects
if(CMAKE_BUILD_TYPE MATCHES "Release")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3")
endif()
set(THIRDPARTY_IGNORED_WARNINGS "-w")
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")
message(STATUS "AppleClang detected - Setting Defaults")
set(CMAKE_CXX_FLAGS
"${CMAKE_CXX_FLAGS} \
-Wall \
-Winit-self \
-ggdb \
-Wextra \
-Wno-cast-align \
-Wcast-qual \
-Wdisabled-optimization \
-Wformat \
-Wextra \
-Woverloaded-virtual \
-Wredundant-decls \
-Wshadow \
-Wsign-promo \
-fdiagnostics-color=always"
)
# pin to AVX for macOS, hopefully all macOS runners have atleast this architecture
# technically speaking, SSE4 is the cutoff for Apple Silicon so...only a matter of time!
if(NOT CMAKE_CXX_COMPILER_TARGET STREQUAL "arm64-apple-darwin")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mavx")
endif()
# additional c++ flags for release mode for our projects
if(CMAKE_BUILD_TYPE MATCHES "Release")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3")
endif()
set(THIRDPARTY_IGNORED_WARNINGS "-w")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-stack_size -Wl,0x20000000")
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
message(STATUS "MSVC detected - Setting Defaults")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /EHsc /utf-8 /arch:AVX")
# Increase stack size for windows, who's default is too low
# Increase the reserved stack size for all threads to 16MB
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /STACK:16000000,16384")
# additional c++ and linker flags for specific build types
if(CMAKE_BUILD_TYPE MATCHES "Release" OR CMAKE_BUILD_TYPE MATCHES "RelWithDebInfo")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /O2 /Ob2")
endif()
if(CMAKE_BUILD_TYPE MATCHES "RelWithDebInfo")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /DEBUG")
endif()
set(THIRDPARTY_IGNORED_WARNINGS "/w")
-Wsign-promo")
else()
message(FATAL_ERROR "Unknown Compiler '${CMAKE_CXX_COMPILER_ID}', get out!")
endif()
set(CMAKE_CXX_FLAGS "/EHsc")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /STACK:10000000")
endif(UNIX)
# Platform Specific Settings
if(WIN32)
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
@@ -159,76 +40,30 @@ if(WIN32)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
endif()
# Code Coverage
option(CODE_COVERAGE "Enable Code Coverage Compiler Flags" OFF)
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CODE_COVERAGE)
include(CodeCoverage)
append_coverage_compiler_flags()
message(STATUS "Code Coverage build is enabled!")
if(ASAN_BUILD)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -O1")
message(STATUS "Doing ASAN build")
endif()
function(build_third_party_lib dir_name target_name)
add_subdirectory(third-party/${dir_name} EXCLUDE_FROM_ALL)
if(CMAKE_CXX_COMPILER_ID MATCHES "MSVC")
# For Microsoft Visual C++ Compiler (MSVC)
target_compile_options(${target_name} PRIVATE /w)
elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
# For GNU Compiler Collection (GCC) or Clang
target_compile_options(${target_name} PRIVATE -w)
endif()
endfunction()
option(CODE_COVERAGE "Enable Code Coverage Compiler Flags" OFF)
set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/third-party/cmake/modules/)
if(UNIX AND CODE_COVERAGE)
include(CodeCoverage)
append_coverage_compiler_flags()
message("Code Coverage build is enabled!")
else()
message("Code Coverage build is disabled!")
endif()
# Dependencies and Libraries
# includes relative to top level jak-project folder
include_directories(./)
# build templating engine library
include_directories(SYSTEM third-party/inja)
# libcurl for HTTP requests
# Enable SSL Support, most URLs now-a-days use SSL!
# TODO - probably integrate with ZSTD since we have it already
set(CURL_USE_LIBSSH2 OFF)
set(CURL_ZLIB OFF)
if(WIN32)
set(CURL_USE_SCHANNEL ON) # native Windows SSL support
elseif(APPLE)
set(CURL_USE_SECTRANSP ON) # native macOS SSL support
else()
if(STATICALLY_LINK)
set(OPENSSL_USE_STATIC_LIBS TRUE)
endif()
set(CURL_USE_OPENSSL ON) # not native, but seems to be the best choice for linux
endif()
include_directories(third-party/curl/include)
if(STATICALLY_LINK)
build_third_party_lib(curl libcurl_static)
else()
build_third_party_lib(curl libcurl_shared)
endif()
# build repl library
build_third_party_lib(replxx replxx)
# SQLite - Jak 2/3's built in editor
add_definitions(-DHAVE_USLEEP=1)
add_definitions(-DSQLITE_THREADSAFE=1)
add_definitions(-DSQLITE_ENABLE_JSON1)
add_definitions(-DSQLITE_ENABLE_RTREE)
build_third_party_lib(sqlite3 sqlite3)
# build tree-sitter parser
include_directories(third-party/tree-sitter/tree-sitter/lib/include)
build_third_party_lib(tree-sitter tree-sitter)
# native OS dialogs for error messages
build_third_party_lib(libtinyfiledialogs libtinyfiledialogs)
# build format library
include_directories(third-party/fmt/include)
build_third_party_lib(fmt fmt)
option(BUILD_SHARED_LIBS "Build shared libraries" ON)
add_subdirectory(third-party/replxx EXCLUDE_FROM_ALL)
# build common library
add_subdirectory(common)
@@ -236,30 +71,15 @@ add_subdirectory(common)
# build decompiler
add_subdirectory(decompiler)
# cubeb - audio
build_third_party_lib(cubeb cubeb)
# build glfw library
add_subdirectory(third-party/glfw)
# build LSP
add_subdirectory(lsp)
# build libco and zstd
build_third_party_lib(libco libco)
build_third_party_lib(zstd libzstd_static)
# build SDL
include(SDLOptions)
if(STATICALLY_LINK)
build_third_party_lib(SDL SDL2-static)
else()
build_third_party_lib(SDL SDL2)
endif()
# build imgui
include_directories(third-party/glad/include)
include_directories(third-party/SDL/include)
build_third_party_lib(imgui imgui)
# build libzip
add_subdirectory(third-party/11zip EXCLUDE_FROM_ALL)
# build the game code in C++
include_directories(third-party/glad/include)
include_directories(third-party/glfw/include)
add_subdirectory(game)
# build the compiler
@@ -270,42 +90,29 @@ add_subdirectory(tools)
# build the gtest libraries
if(WIN32)
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
endif()
build_third_party_lib(googletest gtest)
add_subdirectory(third-party/googletest EXCLUDE_FROM_ALL)
include(GoogleTest)
# build tests
include(test/CMakeLists.txt)
# build lzokay library
build_third_party_lib(lzokay lzokay)
add_subdirectory(third-party/lzokay EXCLUDE_FROM_ALL)
build_third_party_lib(stb_image stb_image)
build_third_party_lib(tiny_gltf tiny_gltf)
build_third_party_lib(xdelta3 xdelta3)
# discord rich presence
include_directories(third-party/discord-rpc/include)
build_third_party_lib(discord-rpc discord-rpc)
# build format library
add_subdirectory(third-party/fmt EXCLUDE_FROM_ALL)
# build zydis third party library for disassembling x86
# TODO - Once under CMake 3.13's policy CMP0077, override with `set()` instead
# NOTE: Once under CMake 3.13's policy CMP0077, override with `set()` instead
option(ZYDIS_BUILD_TOOLS "Zydis: Build tools" OFF)
option(ZYDIS_BUILD_EXAMPLES "Zydis: Build examples" OFF)
option(ZYDIS_BUILD_SHARED_LIB "Zydis: Build shared library" ON)
add_subdirectory(third-party/zydis EXCLUDE_FROM_ALL)
if(STATICALLY_LINK)
message(STATUS "Statically linking zydis")
option(ZYDIS_BUILD_SHARED_LIB "Zydis: Build shared library" OFF)
else()
message(STATUS "dynamically linking zydis")
option(ZYDIS_BUILD_SHARED_LIB "Zydis: Build shared library" ON)
endif()
build_third_party_lib(zydis Zydis)
# windows memory management lib
if(WIN32)
build_third_party_lib(mman mman)
add_subdirectory(third-party/mman)
endif()
-213
View File
@@ -1,213 +0,0 @@
{
"version": 2,
"configurePresets": [
{
"name": "base",
"hidden": true,
"generator": "Ninja",
"architecture": {
"value": "x64",
"strategy": "external"
},
"cacheVariables": {
"INSTALL_GTEST": "True",
"ASAN_BUILD": "OFF",
"STATICALLY_LINK": "OFF",
"ZYDIS_BUILD_SHARED_LIB": "ON",
"CODE_COVERAGE": "OFF",
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
}
},
{
"name": "base-windows-release",
"hidden": true,
"inherits": "base",
"binaryDir": "${sourceDir}/out/build/Release",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"CMAKE_INSTALL_PREFIX": "${sourceDir}/out/install/${presetName}"
}
},
{
"name": "base-windows-relwithdeb",
"hidden": true,
"inherits": "base",
"binaryDir": "${sourceDir}/out/build/Release",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "RelWithDebInfo",
"CMAKE_INSTALL_PREFIX": "${sourceDir}/out/install/${presetName}"
}
},
{
"name": "base-windows-debug",
"hidden": true,
"inherits": "base",
"binaryDir": "${sourceDir}/out/build/Debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_INSTALL_PREFIX": "${sourceDir}/out/install/${presetName}"
}
},
{
"name": "base-linux-release",
"hidden": true,
"inherits": "base",
"binaryDir": "${sourceDir}/build/Release/bin",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install/${presetName}"
}
},
{
"name": "base-linux-debug",
"hidden": true,
"inherits": "base",
"binaryDir": "${sourceDir}/build/Release/bin",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install/${presetName}"
}
},
{
"name": "base-macos-release",
"hidden": true,
"inherits": "base",
"binaryDir": "${sourceDir}/build/Release/bin",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install/${presetName}"
}
},
{
"name": "base-clang",
"hidden": true,
"cacheVariables": {
"CMAKE_C_COMPILER": "clang",
"CMAKE_CXX_COMPILER": "clang++"
}
},
{
"name": "base-msvc",
"hidden": true,
"cacheVariables": {
"CMAKE_C_COMPILER": "cl",
"CMAKE_CXX_COMPILER": "cl"
}
},
{
"name": "base-gcc",
"hidden": true,
"cacheVariables": {
"CMAKE_C_COMPILER": "gcc",
"CMAKE_CXX_COMPILER": "g++"
}
},
{
"name": "Debug-windows-clang",
"displayName": "Windows Debug (clang)",
"description": "Build with Clang with Debug Symbols",
"inherits": ["base-windows-debug", "base-clang"]
},
{
"name": "Debug-windows-clang-static",
"displayName": "Windows Static Debug (clang)",
"description": "Build with Clang as Debug but statically linked",
"inherits": ["base-windows-debug", "base-clang"],
"cacheVariables": {
"STATICALLY_LINK": "true",
"ZYDIS_BUILD_SHARED_LIB": "OFF"
}
},
{
"name": "RelWithDebInfo-windows-clang",
"displayName": "Windows RelWithDebInfo (clang)",
"description": "Build with Clang as Release with Debug Symbols",
"inherits": ["base-windows-relwithdeb", "base-clang"]
},
{
"name": "Release-windows-clang",
"displayName": "Windows Release (clang)",
"description": "Build with Clang as Release without Debug Symbols",
"inherits": ["base-windows-release", "base-clang"]
},
{
"name": "Release-windows-clang-static",
"displayName": "Windows Static Release (clang)",
"description": "Build with Clang as Release but statically linked",
"inherits": ["base-windows-release", "base-clang"],
"cacheVariables": {
"STATICALLY_LINK": "true",
"ZYDIS_BUILD_SHARED_LIB": "OFF"
}
},
{
"name": "Debug-windows-msvc",
"displayName": "Windows Debug (msvc)",
"description": "Build with MSVC's CL with Debug Symbols",
"inherits": ["base-windows-debug", "base-msvc"]
},
{
"name": "Release-windows-msvc",
"displayName": "Windows Release (msvc)",
"description": "Build with MSVC's CL as Release without Debug Symbols",
"inherits": ["base-windows-release", "base-msvc"]
},
{
"name": "Release-linux-clang",
"displayName": "Linux Release (clang)",
"description": "Build with Clang as Release without Debug Symbols",
"inherits": ["base-linux-release", "base-clang"]
},
{
"name": "Release-macos-clang",
"displayName": "MacOS Release (clang)",
"description": "Build with Clang as Release without Debug Symbols",
"inherits": ["base-macos-release", "base-clang"]
},
{
"name": "Release-macos-clang-static",
"displayName": "MacOS Static Release (clang)",
"description": "Build with Clang as Release without Debug Symbols but statically linked",
"inherits": ["base-macos-release", "base-clang"],
"cacheVariables": {
"STATICALLY_LINK": "true",
"ZYDIS_BUILD_SHARED_LIB": "OFF"
}
},
{
"name": "Release-linux-clang-asan",
"displayName": "Linux Release (clang-asan)",
"description": "Build with Clang as Release without Debug Symbols and ASAN Fuzzing",
"inherits": ["base-linux-release", "base-clang"],
"cacheVariables": {
"ASAN_BUILD": "OFF"
}
},
{
"name": "Release-linux-clang-static",
"displayName": "Linux Static Release (clang)",
"description": "Build with Clang as Release without Debug Symbols but statically linked",
"inherits": ["base-linux-release", "base-clang"],
"cacheVariables": {
"STATICALLY_LINK": "true",
"ZYDIS_BUILD_SHARED_LIB": "OFF"
}
},
{
"name": "Debug-linux-clang-static",
"displayName": "Linux Static Release (clang)",
"description": "Build with Clang as Release without Debug Symbols but statically linked",
"inherits": ["base-linux-debug", "base-clang"],
"cacheVariables": {
"STATICALLY_LINK": "true",
"ZYDIS_BUILD_SHARED_LIB": "OFF"
}
},
{
"name": "Release-linux-gcc",
"displayName": "Linux Release (gcc)",
"description": "Build with GCC as Release without Debug Symbols",
"inherits": ["base-linux-release", "base-gcc"]
}
]
}
+42
View File
@@ -0,0 +1,42 @@
{
"configurations": [
{
"name": "Debug",
"generator": "Ninja",
"configurationType": "Debug",
"inheritEnvironments": [ "msvc_x64_x64" ],
"buildRoot": "${projectDir}\\out\\build\\${name}",
"installRoot": "${projectDir}\\out\\install\\${name}",
"cmakeCommandArgs": "",
"buildCommandArgs": "",
"addressSanitizerEnabled": false,
"ctestCommandArgs": "",
"variables": [
{
"name": "INSTALL_GTEST",
"value": "True",
"type": "BOOL"
}
]
},
{
"name": "Release",
"generator": "Ninja",
"configurationType": "Release",
"buildRoot": "${projectDir}\\out\\build\\${name}",
"installRoot": "${projectDir}\\out\\install\\${name}",
"cmakeCommandArgs": "",
"buildCommandArgs": "",
"addressSanitizerEnabled": false,
"ctestCommandArgs": "",
"inheritEnvironments": [ "msvc_x64_x64" ],
"variables": [
{
"name": "INSTALL_GTEST",
"value": "True",
"type": "BOOL"
}
]
}
]
}
-15
View File
@@ -1,15 +0,0 @@
ISC License
Copyright (c) 2020-2022 OpenGOAL Team
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+15
View File
@@ -0,0 +1,15 @@
ISC License
Copyright (c) 2020-2021 water111
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+168 -394
View File
@@ -1,134 +1,56 @@
<p align="center">
<img width="500" height="100%" src="./docs/img/logo-text-colored-new.png">
<img width="500" height="100%" src="./docs/img/logo-text-colored.5cda006b.png">
</p>
<p align="center">
<a href="https://opengoal.dev/docs/intro" rel="nofollow"><img src="https://img.shields.io/badge/Documentation-Here-informational" alt="Documentation Badge" style="max-width:100%;"></a>
<a title="Crowdin" target="_blank" href="https://crowdin.com/project/opengoal"><img src="https://badges.crowdin.net/opengoal/localized.svg"></a>
<a target="_blank" rel="noopener noreferrer" href="https://github.com/open-goal/jak-project/actions/workflows/build-matrix.yaml"><img src="https://github.com/open-goal/jak-project/actions/workflows/build-matrix.yaml/badge.svg" alt="Linux and Windows Build" style="max-width:100%;"></a>
<a href="https://www.codacy.com/gh/open-goal/jak-project/dashboard?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=open-goal/jak-project&amp;utm_campaign=Badge_Grade" rel="nofollow"><img src="https://app.codacy.com/project/badge/Grade/29316d04a1644aa390c33be07289f3f5" alt="Codacy Badge" style="max-width:100%;"></a>
<a href="https://discord.gg/VZbXMHXzWv"><img src="https://img.shields.io/discord/756287461377703987" alt="Discord"></a>
<a href="https://water111.github.io/jak-project/" rel="nofollow"><img src="https://img.shields.io/badge/Documentation-Here-informational" alt="Documentation Badge" style="max-width:100%;"></a>
<a target="_blank" rel="noopener noreferrer" href="https://github.com/water111/jak-project/workflows/Linux/badge.svg"><img src="https://github.com/water111/jak-project/workflows/Linux/badge.svg" alt="Linux" style="max-width:100%;"></a>
<a target="_blank" rel="noopener noreferrer" href="https://github.com/water111/jak-project/workflows/Windows/badge.svg"><img src="https://github.com/water111/jak-project/workflows/Windows/badge.svg" alt="Windows" style="max-width:100%;"></a>
<a href="https://coveralls.io/github/water111/jak-project?branch=master" rel="nofollow"><img src="https://coveralls.io/repos/github/water111/jak-project/badge.svg?branch=master" alt="Coverage Status" style="max-width:100%;"></a>
<a href="https://www.codacy.com/gh/water111/jak-project/dashboard?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=xTVaser/jak-project&amp;utm_campaign=Badge_Grade" rel="nofollow"><img src="https://app.codacy.com/project/badge/Grade/7c3cdc07523f43aca3433484ebc62ff9" alt="Codacy Badge" style="max-width:100%;"></a>
</p>
## Please read first <!-- omit from toc -->
## Table of Contents
> [!IMPORTANT]
> Our repositories on GitHub are primarily for development of the project and tracking active issues. Most of the information you will find here pertains to setting up the project for development purposes and is not relevant to the end-user.
For a setup guide on how to install and play the game there is the following video that you can check out: https://youtu.be/K84UUMnkJc4
For questions or additional information pertaining to the project, we have a Discord for discussion here: https://discord.gg/VZbXMHXzWv
Additionally, you can find further documentation and answers to **frequently asked questions** on the project's main website: https://opengoal.dev
> [!WARNING]
> **Do not use this decompilation project without providing your own legally purchased copy of the game.** We do not distribute any assets from the game - you must use your own legitimately obtained PS2 copy of the game. We support every retail PAL, NTSC, and NTSC-J build, including Greatest Hits copies.
<!-- toc -->
- [Table of Contents](#table-of-contents)
- [Project Description](#project-description)
- [Current Status](#current-status)
- [Methodology](#methodology)
- [Setting up a Development Environment](#setting-up-a-development-environment)
- [Docker](#docker)
- [Linux](#linux)
- [Ubuntu (20.04)](#ubuntu-2004)
- [Arch](#arch)
- [Fedora](#fedora)
- [Windows](#windows)
- [Required Software](#required-software)
- [Using Visual Studio](#using-visual-studio)
- [MacOS](#macos)
- [Intel Based](#intel-based)
- [Apple Silicon](#apple-silicon)
- [VSCode](#vscode)
- [Building and Debugging](#building-and-debugging)
- [Building and Running the Game](#building-and-running-the-game)
- [Extract Assets](#extract-assets)
- [Build the Game](#build-the-game)
- [Run the Game](#run-the-game)
- [Connecting the REPL to the Game](#connecting-the-repl-to-the-game)
- [Running the Game Without Auto-Booting](#running-the-game-without-auto-booting)
- [Interacting with the Game](#interacting-with-the-game)
- [Technical Project Overview](#technical-project-overview)
- [`goalc`](#goalc)
- [Running the compiler](#running-the-compiler)
- [`decompiler`](#decompiler)
- [Running the decompiler](#running-the-decompiler)
- [`goal_src/`](#goal_src)
- [`game` runtime](#game-runtime)
- [Getting Started - Linux (Ubuntu)](#getting-started---linux-ubuntu)
- [Getting Started - Linux (Arch)](#getting-started---linux-arch)
- [Getting Started - Nixpkgs](#getting-started---nixpkgs)
- [Getting Started - Windows](#getting-started---windows)
- [Project Layout](#project-layout)
- [Directory Layout](#directory-layout)
- [More Documentation](#more-documentation)
- [ASan Build](#asan-build)
- [On Windows / Visual Studio](#on-windows--visual-studio)
<!-- tocstop -->
## Project Description
This project is to port the original Jak and Daxter and Jak II to PC. Over 98% of the games are written in GOAL, a custom Lisp language developed by Naughty Dog. Our strategy is:
This project is to port Jak 1 (NTSC, "black label" version) to PC. Over 99% of this game is written in GOAL, a custom Lisp language developed by Naughty Dog. Our strategy is:
- decompile the original game code into human-readable GOAL code
- develop our own compiler for GOAL and recompile game code for x86-64
- create a tool to extract game assets into formats that can be easily viewed or modified
- create tools to repack game assets into a format that our port uses.
Our objectives are:
- make the port a "native application" on x86-64, with high performance. It shouldn't be emulated, interpreted, or transpiled.
- make the port a "native application" on x86-64, with high performance. It shouldn't emulated, interpreted, or transpiled.
- Our GOAL compiler's performance should be around the same as unoptimized C.
- try to match things from the original game and development as possible. For example, the original GOAL compiler supported live modification of code while the game is running, so we do the same, even though it's not required for just porting the game.
- support modifications. It should be possible to make edits to the code without everything else breaking.
We support both Linux and Windows on x86-64.
> We do not support, or plan to support the ARM architecture. This means that this will not run on devices such as an M1 Mac or a mobile device.
### Current Status
Jak 1 is largely playable from start to finish with a handful of bugs that are continually being ironed out. Jak 2 is in development.
![](./docs/img/promosmall1.png)
![](./docs/img/promosmall2.png)
YouTube playlist:
https://www.youtube.com/playlist?list=PLWx9T30aAT50cLnCTY1SAbt2TtWQzKfXX
### Methodology
To help with decompiling, we've built a decompiler that can process GOAL code and unpack game assets. We manually specify function types and locations where we believe the original code had type casts (or where they feel appropriate) until the decompiler succeeds, then we clean up the output of the decompiled code by adding comments and adjusting formatting, then save it in `goal_src`.
Our decompiler is designed specifically for processing the output of the original GOAL compiler. As a result, when given correct casts, it often produces code that can be directly fed into a compiler and works perfectly. This is continually tested as part of our unit tests.
## Setting up a Development Environment
The remainder of this README is catered towards people interested in building the project from source, typically with the intention on contributing as a developer.
If this does not sound like you and you just want to play the game, refer to the above section [Quick Start](#quick-start)
### Docker
All three Linux systems are supported using Docker.
Pick your supported prefered flavour of linux and build your chosen image
```
docker build -f docker/(Arch|Fedora|Ubuntu)/Dockerfile -t jak .
```
This will create an image with all required dependencies and already built.
```
docker run -v "$(pwd)"/build:/home/jak/jak-project/build -it jak bash
```
Note: If you change the content of the `build/` directory you'll need to rerun the `build` command. Alternatively you can get the build via `docker cp`.
This will link your `build/` folder to the images so can validate your build or test it on an external device.
Docker images can be linked into your IDE (e.g. CLion) to help with codesniffing, static analysis, run tests and continuous build.
Unfortunately you'll still need task runner on your local machine to run the game or instead, manually run the game via the commands found in `Taskfile.yml`.
### Linux
#### Ubuntu (20.04)
## Getting Started - Linux (Ubuntu)
Install packages and init repository:
```sh
sudo apt install gcc make cmake build-essential g++ nasm clang-format libxrandr-dev libxinerama-dev libxcursor-dev libpulse-dev libxi-dev python libgl1-mesa-dev libssl-dev
sudo sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b /usr/local/bin
sudo apt install gcc make cmake build-essential g++ nasm clang-format
git submodule update --init --recursive
```
Compile:
@@ -143,29 +65,25 @@ Run tests:
./test.sh
```
Note: we have found that `clang` and `lld` are significantly faster to compile and link than `gcc`, generate faster code, and have better warning messages. To install these:
```sh
Note: we have found that `clang` and `lld` are significantly faster to compile and link than `gcc`, generate faster code, and have better warning messages. To install these:
```
sudo apt install lld clang
```
and run `cmake` (in a fresh build directory) with:
```sh
```
cmake -DCMAKE_SHARED_LINKER_FLAGS="-fuse-ld=lld" -DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=lld" -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ ..
```
this decreases the compile and link time from ~10 seconds to ~4 seconds.
#### Arch
## Getting Started - Linux (Arch)
Install packages and init repository:
```sh
sudo pacman -S cmake libpulse base-devel nasm python libx11 libxrandr libxinerama libxcursor libxi
yay -S go-task
sudo pacman -S gcc make cmake base-devel g++ nasm
git submodule update --init --recursive
```
For Arch only, replace `task` with `go-task` in the rest of the instructions.
Compile:
```sh
@@ -178,311 +96,167 @@ Run tests:
./test.sh
```
#### Fedora
## Getting Started - Nixpkgs
Install packages and init repository:
If your Nix supports flakes:
```sh
sudo dnf install cmake python lld clang nasm libX11-devel libXrandr-devel libXinerama-devel libXcursor-devel libXi-devel pulseaudio-libs-devel mesa-libGL-devel
sudo sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b /usr/local/bin
nix develop # development environment
nix build # package
nix develop '.#jak-asan-dev' # development environment with Clang
nix build '.#jak-asan' # package with Clang ASan build
```
Compile with `clang`:
Otherwise, with traditional Nix:
```sh
cmake -DCMAKE_SHARED_LINKER_FLAGS="-fuse-ld=lld" -DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=lld" -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -B build
cmake --build build -j$(nproc)
nix-shell # development environment
nix-build # package
nix-shell -A packages.x86_64-linux.jak-asan-dev # development environment with Clang
nix-build -A packages.x86_64-linux.jak-asan # package with Clang ASan build
```
Run tests:
## Getting Started - Windows
Install Visual Studio 2019 and get the C++ and CMake tools via the Visual Studio Installer
On Windows, it's recommended to get Scoop to use as a package manager, making the follow steps _much_ easier. Follow the steps on the bottom of the homepage here https://scoop.sh/
Once Scoop is installed, run the following command:
```ps1
scoop install llvm nasm
```
Initialize the repository's third-party dependencies:
```sh
./test.sh
git submodule update --init --recursive
```
### Windows
Open the project as a CMake project, browse for the root level `CMakeLists.txt`:
#### Required Software
![](./docs/markdown/imgs/open-cmake-vs.png)
We primarily use Visual Studio on Windows for C++ development. Download the latest community edition from [here](https://visualstudio.microsoft.com/vs/). At the time of writing this is Visual Studio 2022.
In the toolbar, you should be able to select an individual component to compile, or combine within the root CMakeLists.txt. In the future we will pre-define configurations to make this easier.
You will require the `Desktop development with C++` workload. This can be selected during the installation, or after via the `Visual Studio Installer`, modifying the Visual Studio Installation.
![](./docs/markdown/imgs/cmake-build-vs.png)
On Windows, it's recommended to use a package manager, we use Scoop. Follow the steps on the bottom of the homepage [here](https://scoop.sh/) to get it.
You may also wish to view the files that pertain to each CMake target, rather than the project as it is normally:
Once Scoop is installed, run the following commands:
![](./docs/markdown/imgs/cmake-target-view.png)
```sh
scoop install git llvm nasm python task
```
#### Using Visual Studio
Clone the repository by running the following command in your folder of choice.
```sh
git clone https://github.com/open-goal/jak-project.git
```
This will create a `jak-project` folder, open the project as a CMake project via Visual Studio.
![](./docs/img/windows/open-project.png)
Then build the entire project as `Windows Release (clang)`. You can also press Ctrl+Shift+B as a hotkey for Build All. We currently prefer `clang` on Windows as opposed to `msvc`, though it should work as well!
![](./docs/img/windows/release-build.png)
![](./docs/img/windows/build-all.png)
### MacOS
> NOTE: At this time you can only run the game on macOS if you have an Intel processor.
Ensure that you have Xcode command line tools installed (this installs things like Apple Clang). If you don't, you can run the following command:
```bash
xcode-select --install
```
#### Intel Based
```bash
brew install go-task/tap/go-task
brew install cmake nasm ninja go-task clang-format
cmake -B build --preset=Release-macos-clang
cmake --build build --parallel $((`sysctl -n hw.logicalcpu`))
```
#### Apple Silicon
**Not Supported at This Time**
```bash
brew install go-task/tap/go-task
brew install cmake ninja go-task clang-format
cmake -B build --preset=Release-macos-clang
cmake --build build --parallel $((`sysctl -n hw.logicalcpu`))
```
You may have to add the MacOS SDK to your `LIBRARY_PATH`:
- `export LIBRARY_PATH="$LIBRARY_PATH:/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib"`
### VSCode
If you either don't want to or cannot use Visual Studio for working with the C++ project, VSCode is a good alternatively.
The `clangd` extension is [recommended](https://marketplace.visualstudio.com/items?itemName=llvm-vs-code-extensions.vscode-clangd) and requires `clangd` to be on your `$PATH`. If you can run `clangd` in a terminal successfully then you should be good to go.
Once you generate your CMake for the first time the clangd LSP should be able to index the project and give you intellisense.
#### Building and Debugging
TODO - Consider Contributing Documentation :)
### Building and Running the Game
Getting a running game involves 4 steps:
1. Build C++ tools (follow Getting Started steps above for your platform)
2. Extract assets from the game
3. Build the game
4. Run the game
#### Extract Assets
First, setup your settings so the following scripts know which game you are using, and which version. For the black label version of the game, run the following in a terminal:
```sh
task set-game-jak1
task set-decomp-ntscv1
```
For other versions of the game, you will need to use a different `-set-decomp-<VERSION>` command. An example for the PAL version:
```sh
task set-game-jak1
task set-decomp-pal
```
> Run `task --list` to see the other available options
> At the time of writing, only Jak 1 is expected to work end-to-end!
The first step is to extract your ISO file contents into the `iso_data/<game-name>` folder. In the case of Jak 1 this is `iso_data/jak1`.
Once this is done, open a terminal in the `jak-project` folder and run the following:
```sh
task extract
```
#### Build the Game
The next step is to build the game itself. To do so, in the same terminal run the following:
```sh
task repl
```
You will be greeted with a prompt like so:
```sh
_____ _____ _____ _____ __
| |___ ___ ___| __| | _ | |
| | | . | -_| | | | | | | |__
|_____| _|___|_|_|_____|_____|__|__|_____|
|_|
Welcome to OpenGOAL 0.8!
Run (repl-help) for help with common commands and REPL usage.
Run (lt) to connect to the local target.
g >
```
Run the following to build the game:
```sh
g > (mi)
```
> IMPORTANT NOTE! If you're not using the non-default version of the game, you may hit issues trying to run `(mi)` in this step. An example error might include something like:
>
> `Input file iso_data/jak1/MUS/TWEAKVAL.MUS does not exist.`
>
> This is because the decompiler inputs/outputs using the `gameName` JSON field in the decompiler config. For example if you are using Jak 1 PAL, it will assume `iso_data/jak1_pal` and `decompiler_out/jak1_pal`. Therefore, you can inform the REPL/compiler of this via the `gameVersionFolder` config field described [here](./goal_src/user/README.md)
#### Run the Game
Finally the game can be ran. Open a second terminal from the `jak-project` directory and run the following:
```sh
task boot-game
```
The game should boot automatically if everything was done correctly.
##### Connecting the REPL to the Game
Connecting the REPL to the game allows you to inspect and modify code or data while the game is running.
To do so, in the REPL after a successful `(mi)`, run the following:
```sh
g > (lt)
```
If successful, your prompt should change to:
```sh
gc>
```
For example, running the following will print out some basic information about Jak:
```sh
gc> *target*
```
##### Running the Game Without Auto-Booting
You can also start up the game without booting. To do so run the following in one terminal
```sh
task run-game
```
And then in your REPL run the following (after a successful `(mi)`):
```sh
g > (lt)
[Listener] Socket connected established! (took 0 tries). Waiting for version...
Got version 0.8 OK!
[Debugger] Context: valid = true, s7 = 0x147d24, base = 0x2123000000, tid = 2438049
gc> (lg)
10836466 #xa559f2 0.0000 ("game" "kernel")
gc> (test-play)
(play :use-vis #t :init-game #f) has been called!
0 #x0 0.0000 0
gc>
```
#### Interacting with the Game
In the graphics window, you can use the period key to bring up the debug menu. Controllers also work, using the same mapping as the original game.
Check out the `pc_debug`, `examples` and `pc` folders under `goal_src` for some examples of GOAL code we wrote. The debug files that are not loaded automatically by the engine have instructions for how to run them.
## Technical Project Overview
## Project Layout
There are four main components to the project.
1. `goalc` - the GOAL compiler for x86-64
2. `decompiler` - our decompiler
3. `goal_src/` - the folder containing all OpenGOAL / GOOS code
4. `game` - aka the runtime written in C++
The first is `goalc`, which is a GOAL compiler for x86-64. Our implementation of GOAL is called OpenGOAL. All of the compiler source code is in `goalc`. To run the compiler on Linux, there is a script `gc.sh`. The compiler is controlled through a prompt which can be used to enter commands to compile, connect to a running GOAL program for interaction, run the OpenGOAL debugger, or, if you are connected to a running GOAL program, can be used as a REPL to run code interactively. In addition to compiling code files, the compiler has features to pack and build data files.
Let's break down each component.
The second component to the project is the decompiler. You must have a copy of the PS2 game and place all files from the DVD into the `iso_data` folder. Then run `decomp.sh` to run the decompiler. The decompile will extract assets to the `assets` folder. These assets will be used by the compiler when building the port. The decompiler will output code and other data intended to be inspected by humans in the `decompiler_out` folder. Stuff in this folder will not be used by the compiler.
### `goalc`
The third is the game source code, written in OpenGOAL. This is located in `goal_src`. All GOAL and GOOS code should be in this folder. Right now most of this is placeholders, but you can take a look at `kernel/gcommon.gc` or `goal-lib.gc` to see some in-progress source code.
Our implementation of GOAL is called OpenGOAL.
The final component is the "runtime", located in `game`. This is the part of the game that's written in C++. In the port, that includes:
- The "C Kernel", which contains the GOAL linker and some low-level GOAL language features. GOAL has a completely custom dynamically linked object file format so in order to load the first GOAL code, you need a linker written in C++. Some low-level functions for memory allocation, communicating with the I/O Processor, symbol table, strings, and the type system are also implemented in C, as these are required for the linker. It also listens for incoming messages from the compiler and passes them to the running game. This also initializes the game, by initializing the PS2 hardware, allocating the GOAL heaps, loading the GOAL kernel off of the DVD, and executing the kernel dispatcher function. This is in the `game/kernel` folder. This should be as close as possible to the game, and all differences should be noted with a comment.
All of the compiler source code is in `goalc/`. The compiler is controlled through a prompt which can be used to enter commands to compile, connect to a running GOAL program for interaction, run the OpenGOAL debugger, or, if you are connected to a running GOAL program, can be used as a REPL to run code interactively. In addition to compiling code files, the compiler has features to pack and build data files.
#### Running the compiler
**Environment Agnostic**
If you have installed `task` as recommended above, you can run the compiler with `task repl`
**Linux**
To run the compiler on Linux, there is a script `scripts/shell/gc.sh`.
**Windows**
On Windows, there is a `scripts/batch/gc.bat` scripts and a `scripts/batch/gc-no-lt.bat` script, the latter of which will not attempt to automatically attach to a running target.
### `decompiler`
The second component to the project is the decompiler.
The decompiler will output code and other data intended to be inspected by humans in the `decompiler_out` folder. Files in this folder will not be used by the compiler.
#### Running the decompiler
You must have a copy of the PS2 game and place all files from the DVD inside a folder corresponding to the game within `iso_data` folder (`jak1` for Jak 1 Black Label, etc.), as seen in this picture:
![](./docs/img/iso_data-help.png)
The decompiler will extract assets to the `assets` folder. These assets will be used by the compiler when building the port, and you may want to turn asset extraction off after running it once.
**Environment Agnostic**
If you have installed `task` as recommended above, you can run the compiler with `task decomp`
**Linux**
To run, you can use `scripts/shell/decomp.sh` to run the decompiler
**Windows**
To run, you can use `scripts/shell/decomp-jak1.bat` to run the decompiler
### `goal_src/`
The game source code, written in OpenGOAL, is located in `goal_src`. All GOAL and GOOS code should be in this folder.
### `game` runtime
The final component is the "runtime", located in `game`. This is the part of the game that's written in C++.
In the port, that includes:
- The "C Kernel", which contains the GOAL linker and some low-level GOAL language features. GOAL has a completely custom dynamically linked object file format so in order to load the first GOAL code, you need a linker written in C++. Some low-level functions for memory allocation, communicating with the I/O Processor, symbol table, strings, and the type system are also implemented in C, as these are required for the linker. It also listens for incoming messages from the compiler and passes them to the running game. This also initializes the game, by initializing the PS2 hardware, allocating the GOAL heaps, loading the GOAL kernel off of the DVD, and executing the kernel dispatcher function. This is in the `game/kernel` folder. This should be as close as possible to the game, and all differences should be noted with a comment.
- Implementation of Sony's standard library. GOAL code can call C library functions, and Naughty Dog used some Sony library functions to access files, memory cards, controllers, and communicate with the separate I/O Processor. The library functions are in `game/sce`. Implementations of library features specific to the PC port are located in `game/system`.
- The I/O Processor driver, OVERLORD. The PS2 had a separate CPU called the I/O Processor (IOP) that was directly connected to the DVD drive hardware and the sound hardware. Naughty Dog created a custom driver for the IOP that handled streaming data off of the DVD. It is much more complicated than I first expected. It's located in `game/overlord`. Like the C kernel, we try to keep this as close as possible to the actual game.
- Sound code. Naughty Dog used a third party library for sound called `989SND`. Code for the library and an interface for it is located in `game/sound`.
- PC specific graphics code. We have a functional OpenGL renderer and context that can create a game window and display graphics on it. The specific renderers used by the game however are mostly implemented. Aside from post-processing effects, everything in the game is rendered. This is located in `game/graphics`. While many liberties will be taken to make this work, the end result should very closely match the actual game.
- Extra assets used by the port in some fashion, located in `game/assets`. These include extra text files, icons, etc.
- The I/O Processor driver, Overlord. The PS2 had a separate CPU called the I/O Processor (IOP) that was directly connected to the DVD drive hardware and the sound hardware. Naughty Dog created a custom driver for the IOP that handled streaming data off of the DVD. It is much more complicated than I first expected. It's located in `game/overlord`. Like the C kernel, we try to keep this as close as possible to the actual game.
- Sound Code. Naughty Dog used a third party library for sound. We have not started on this yet.
- PC specific graphics stuff. We have not started on this yet.
## Directory Layout
- `.github`: GitHub actions CI setup
- `.vs`: Visual Studio project configurations
- `assets`: extracted assets (textures, translated game text) generated by the decompiler. Not included in the repository. To be used when building the PC port.
- `build`: C++ CMake build folder
- `common`: common C++ code shared between the compiler, decompiler, and game.
- `cross_os_debug`: platform-independent library for implementing the OpenGOAL debugger. Linux-only currently
- `cross_sockets`: platform-independent library for sockets. Used to connect the compiler to a running game. Linux and Windows.
- `goos`: the compiler-time macro language and parser for OpenGOAL.
- `type_system`: the OpenGOAL type system
- `util`: Random utility functions for accessing files, timers, etc.
- `decompiler`: Source code for the decompiler
- `config`: JSON config files for the decompiler and type definition file.
- `data`: utilities to extract assets from the game
- `Disasm`: MIPS disassembler
- `Function`: Tools for analyzing GOAL functions
- `gui`: an early prototype of a Python GUI for reading the output of the decompiler
- `IR`: the "Intermediate Representation" for GOAL functions
- `ObjectFile`: Utilities for processing the GOAL object file format.
- `scripts`: Useful scripts for setting up the decompilation
- `util`: random utilities
- `decompiler_out`: output of the decompiler that's not automatically used by the compiler. This is for humans to read and use. Not included in the repository.
- `doc`: more documentation!
- `game`: the source code for the game executable
- `common`: shared stuff between the `kernel` and `overlord`
- `kernel`: the part of the GOAL kernel written in C. The entry point for the game is in `kboot.cpp`.
- `overlord`: the I/O processor driver used to get data off of the DVD
- `sce`: the Sony library implementation
- `system`: PC-port specific stuff
- `goal_src`: The GOAL code for the game. It's mostly empty now.
- `build`: info related to the GOAL build system.
- `engine`: the game engine
- `kernel`: The GOAL kernel
- `levels`: Level specific code.
- `goalc`: The OpenGOAL compiler
- `compiler`: The implementation of the OpenGOAL language
- `data_compiler`: Tools for packing data
- `debugger`: The OpenGOAL debugger (part of the compiler)
- `emitter`: x86-64 emitter and object file generator
- `listener`: The OpenGOAL listener, which connects the compiler to a running GOAL program for the interactive REPL
- `regalloc`: Register allocator
- `iso_data`:
- `out`: Outputs from the build process. Only the `iso` subfolder should contain assets used by the game.
- `iso`: Final outputs that are used by the game.
- `obj`: Object files generated by the compiler.
- `resources`: To be removed. Contains fake versions of some files required to get things booting.
- `scripts`: Utility scripts.
- `test`: Unit tests (run on GitHub Actions)
- `third-party`: Third party libraries
- CMake Code Coverage. For code coverage statistics on GitHub builds
- `fmt`. String formatting library
- `googletest`: Test framework
- `inja`: templating library used for generating test code for compiler tests
- `minilzo`: decompression code for Jak 2 and later DGOs
- `mman`: Windows library used to emulate `mmap` on Linux
- `run-clang-format`: Utility to check and enforce code formatting
- `run-clang-tidy`
- `zydis`: x86-64 disassembler used in the OpenGOAL debugger
- `json`: A JSON library
- `replxx`: Used for the REPL input. Support history and useful editing shortcuts.
- `svpng`: Save a PNG file
## More Documentation
Check out these files for more documentation. Some of it is still in progress
- `doc/goal_dbg_doc.md`: OpenGOAL debugger
- `doc/goal_doc.md`: OpenGOAL language
- `doc/reader.md`: OpenGOAL "reader" documentation (OpenGOAL syntax)
- `doc/type_system.md`: OpenGOAL type system documentation
- `doc/porting_to_x86.md`: Summary of changes we're making to port to x86-64
- `doc/goos.md`: GOOS macro language
## ASan Build
The project supports building with Address Sanitizer (https://github.com/google/sanitizers/wiki/AddressSanitizer) in Linux.
```sh
export CXX=clang++
cmake .. -DASAN_BUILD=TRUE
```
You will have to delete the build folder when changing compilers. When running `cmake`, you should see something like:
```
-- The CXX compiler identification is Clang 10.0.0
...
-- Doing ASAN build
```
Then you can run the tests, runtime, and compiler as normal and they will abort if ASan finds an error.
### On Windows / Visual Studio
Until 16.9 Preview 4, when attaching a debugger to the ASan build, you must disable breaking on Win32 Access Violation exceptions. See the relevant section `Debugging - Exceptions` here https://devblogs.microsoft.com/cppblog/asan-for-windows-x64-and-debug-build-support/#known-issues
+35 -170
View File
@@ -1,194 +1,59 @@
version: 3
includes:
build: ./scripts/tasks/Taskfile_{{OS}}.yml
dotenv:
- ./scripts/tasks/.env
- ./scripts/tasks/.env.default
version: '3'
tasks:
# SETTINGS / CONFIGURATION
settings:
- 'python ./scripts/tasks/update-env.py --info'
set-game-jak1:
- 'python ./scripts/tasks/update-env.py --game jak1'
set-game-jak2:
- 'python ./scripts/tasks/update-env.py --game jak2'
set-game-jak3:
- 'python ./scripts/tasks/update-env.py --game jak3'
set-decomp-ntscv1:
desc: "aka black label"
cmds:
- 'python ./scripts/tasks/update-env.py --decomp_config ntscv1'
set-decomp-ntscv2:
desc: "aka red label"
cmds:
- 'python ./scripts/tasks/update-env.py --decomp_config ntscv2'
set-decomp-pal:
desc: "PAL region version"
cmds:
- 'python ./scripts/tasks/update-env.py --decomp_config pal'
set-decomp-ntscjp:
desc: "NTSC-J region version"
cmds:
- 'python ./scripts/tasks/update-env.py --decomp_config ntscjp'
set-decomp-ntscko:
desc: "NTSC-K region version"
cmds:
- 'python ./scripts/tasks/update-env.py --decomp_config ntscko'
# GENERAL
extract:
desc: "Extracts the game's assets from './iso_data' with the set decompiler config"
preconditions:
- sh: test -f {{.DECOMP_BIN_RELEASE_DIR}}/decompiler{{.EXE_FILE_EXTENSION}}
msg: "Couldn't locate decompiler executable in '{{.DECOMP_BIN_RELEASE_DIR}}/decompiler'"
cmds:
- "{{.DECOMP_BIN_RELEASE_DIR}}/decompiler \"./decompiler/config/{{.DECOMP_CONFIG}}\" \"./iso_data\" \"./decompiler_out\" --version \"{{.DECOMP_CONFIG_VERSION}}\" --config-override '{\"decompile_code\": false, \"levels_extract\": true, \"allowed_objects\": []}'"
boot-game:
desc: "Boots the game, it will fail if it's not already compiled!"
preconditions:
- sh: test -f {{.GK_BIN_RELEASE_DIR}}/gk{{.EXE_FILE_EXTENSION}}
msg: "Couldn't locate runtime executable in '{{.GK_BIN_RELEASE_DIR}}/gk'"
cmds:
- "{{.GK_BIN_RELEASE_DIR}}/gk -v --game {{.GAME}} -- -boot -fakeiso -debug"
boot-game-retail:
desc: "Boots the game without debug mode, it will fail if it's not already compiled!"
preconditions:
- sh: test -f {{.GK_BIN_RELEASE_DIR}}/gk{{.EXE_FILE_EXTENSION}}
msg: "Couldn't locate runtime executable in '{{.GK_BIN_RELEASE_DIR}}/gk'"
cmds:
- "{{.GK_BIN_RELEASE_DIR}}/gk -v --game {{.GAME}} -- -boot -fakeiso"
run-game:
desc: "Start the game's runtime, to start the game itself the REPL is required"
preconditions:
- sh: test -f {{.GK_BIN_RELEASE_DIR}}/gk{{.EXE_FILE_EXTENSION}}
msg: "Couldn't locate runtime executable in '{{.GK_BIN_RELEASE_DIR}}/gk'"
cmds:
- "{{.GK_BIN_RELEASE_DIR}}/gk -v --game {{.GAME}} -- -fakeiso -debug"
# DEVELOPMENT
repl:
desc: "Start the REPL"
preconditions:
- sh: test -f {{.GOALC_BIN_RELEASE_DIR}}/goalc{{.EXE_FILE_EXTENSION}}
msg: "Couldn't locate compiler executable in '{{.GOALC_BIN_RELEASE_DIR}}/goalc'"
cmds:
- "{{.GOALC_BIN_RELEASE_DIR}}/goalc --user-auto --game {{.GAME}}"
format-json:
desc: "Format code"
cmds:
# npm install -g prettier
- cmd: npx prettier --write ./decompiler/config/jak*/**/*.jsonc
ignore_error: true
format:
desc: "Format code"
cmds:
- cmd: python ./scripts/cpp/format-includes.py
- cmd: python ./scripts/ci/lint-trailing-whitespace.py --fix
- cmd: python ./third-party/run-clang-format/run-clang-format.py -r common decompiler game goalc test tools lsp -i
- task: format-json
format-gsrc:
desc: "Run formatter on gsrc file"
preconditions:
- sh: test -f {{.FORMATTER_BIN_RELEASE_DIR}}/formatter{{.EXE_FILE_EXTENSION}}
msg: "Couldn't locate formatter executable in '{{.FORMATTER_BIN_RELEASE_DIR}}/formatter'"
- python ./third-party/run-clang-format/run-clang-format.py -r common decompiler game goalc test -i
run-game:
cmds:
- "{{.FORMATTER_BIN_RELEASE_DIR}}/formatter --write --file '{{.FILE}}'"
# DECOMPILING
- ./out/build/Debug/bin/gk.exe -fakeiso -debug -nodisplay
repl:
cmds:
- ./out/build/Release/bin/goalc.exe -auto-lt
decomp:
cmds:
- "{{.DECOMP_BIN_RELEASE_DIR}}/decompiler \"./decompiler/config/{{.DECOMP_CONFIG}}\" \"./iso_data\" \"./decompiler_out\" --version \"{{.DECOMP_CONFIG_VERSION}}\" --config-override '{\"decompile_code\": true, \"levels_extract\": false}'"
decomp-file:
cmds:
- "{{.DECOMP_BIN_RELEASE_DIR}}/decompiler \"./decompiler/config/{{.DECOMP_CONFIG}}\" \"./iso_data\" \"./decompiler_out\" --version \"{{.DECOMP_CONFIG_VERSION}}\" --config-override '{\"decompile_code\": true, \"levels_extract\": false, \"allowed_objects\": [\"{{.FILE}}\"]}'"
- ./out/build/Release/bin/decompiler.exe "./decompiler/config/jak1_ntsc_black_label.jsonc" "./iso_data/jak1" "./decompiler_out/jak1"
decomp-clean:
cmds:
- python ./scripts/tasks/clean-decomp.py --game "{{.GAME}}"
lint-gsrc-file:
- rm ./decompiler_out/**/*.asm
- rm ./decompiler_out/**/*disasm.gc
decomp-file:
cmds:
- python ./scripts/gsrc/lint-gsrc-file.py --game {{.GAME}} --file "{{.FILE}}"
update-gsrc:
- python ./scripts/next-decomp-file.py --file "{{.FILE}}"
- task: decomp
decomp-list:
cmds:
- python ./scripts/gsrc/update-gsrc-via-refs.py --game "{{.GAME}}" --decompiler "{{.DECOMP_BIN_RELEASE_DIR}}/decompiler" --decompiler_config {{.DECOMP_CONFIG}} --version {{.DECOMP_CONFIG_VERSION}}
update-gsrc-glob:
- python ./scripts/next-decomp-file.py --list "{{.LIST}}"
vars:
LIST: '{{default "0" .LIST}}'
# python -m pip install -U watchdog[watchmedo]
decomp-watch:
cmds:
- python ./scripts/gsrc/update-gsrc-via-refs.py --game "{{.GAME}}" --file_pattern "{{.GLOB}}" --decompiler "{{.DECOMP_BIN_RELEASE_DIR}}/decompiler" --decompiler_config {{.DECOMP_CONFIG}} --version {{.DECOMP_CONFIG_VERSION}}
update-gsrc-file:
- watchmedo shell-command --drop --patterns="*.gc;*.jsonc" --recursive --command='task decomp-file FILE="{{.FILE}}"' ./decompiler/config/
clean-all-types:
cmds:
- task: decomp-file
- python ./scripts/gsrc/update-from-decomp.py --game "{{.GAME}}" --file "{{.FILE}}"
- task: lint-gsrc-file
copy-common-naming:
cmds:
- python ./scripts/gsrc/copy-common-naming.py --file "{{.FILE}}" --decompiler "{{.DECOMP_BIN_RELEASE_DIR}}/decompiler"
- task: format-json
copy-common-naming-from-refs:
cmds:
- python ./scripts/gsrc/copy-common-naming.py --update-names-from-refs --decompiler "{{.DECOMP_BIN_RELEASE_DIR}}/decompiler"
- task: format-json
# ASSET RIPPING
rip-textures:
cmds:
- "{{.DECOMP_BIN_RELEASE_DIR}}/decompiler \"./decompiler/config/{{.DECOMP_CONFIG}}\" \"./iso_data\" \"./decompiler_out\" --version \"{{.DECOMP_CONFIG_VERSION}}\" --config-override '{\"decompile_code\": false, \"levels_extract\": true, \"save_texture_pngs\": true}'"
rip-levels:
cmds:
- "{{.DECOMP_BIN_RELEASE_DIR}}/decompiler \"./decompiler/config/{{.DECOMP_CONFIG}}\" \"./iso_data\" \"./decompiler_out\" --version \"{{.DECOMP_CONFIG_VERSION}}\" --config-override '{\"decompile_code\": false, \"levels_extract\": true, \"rip_levels\": true}'"
rip-collision:
cmds:
- "{{.DECOMP_BIN_RELEASE_DIR}}/decompiler \"./decompiler/config/{{.DECOMP_CONFIG}}\" \"./iso_data\" \"./decompiler_out\" --version \"{{.DECOMP_CONFIG_VERSION}}\" --config-override '{\"decompile_code\": false, \"levels_extract\": true, \"extract_collision\": true, \"rip_collision\": true}'"
# TOOLS
- python ./scripts/cleanup-all-types.py
analyze-ee-memory:
cmds:
- python ./scripts/tasks/extract-zip.py --file "{{.FILE}}" --out ./savestate_out/
- '{{.MEMDUMP_BIN_RELEASE_DIR}}/memory_dump_tool ./savestate_out/eeMemory.bin --output-path ./ --game {{.GAME}} > ee-analysis.log'
- ./out/build/Release/bin/memory_dump_tool.exe "{{.FILE}}" ./ > ee-analysis.log
watch-pcsx2:
# python -m pip install -U "watchdog[watchmedo]"
cmds:
- watchmedo shell-command --drop --patterns="*.p2s" --recursive --command='task analyze-ee-memory FILE="${watch_src_path}"' "{{.SAVESTATE_DIR}}"
vars:
SAVESTATE_DIR: '{{default "." .SAVESTATE_DIR}}'
type-search:
desc: Just an example to show it running
offline-tests:
cmds:
- "{{.TYPESEARCH_BIN_RELEASE_DIR}}/type_searcher --output-path ./search-results.json --game {{.GAME}} --fields '[{\"type\":\"int16\",\"offset\":2},{\"type\":\"int16\",\"offset\":4}]'"
update-treesitter:
desc: Updates locally built tree-sitter rules
- ./out/build/Release/bin/offline-test.exe "./iso_data/jak1"
add-reference-test:
cmds:
- cd ../tree-sitter-opengoal && yarn gen
- python ./scripts/tasks/cp.py --src "../tree-sitter-opengoal/src/*" --dest "./third-party/tree-sitter/tree-sitter-opengoal"
- python ./scripts/tasks/cp.py --src "../tree-sitter-opengoal/grammar.js" --dest "./third-party/tree-sitter/tree-sitter-opengoal"
fix-translations:
desc: Finds and tries to fix invalid translation Characters
- task: decomp-file
- python ./scripts/add-reference-test.py --file "{{.FILE}}"
- task: offline-tests
update-reference-tests:
cmds:
- python ./scripts/ci/lint-characters.py --fix
lint:
cmds:
- python ./scripts/ci/lint-trailing-whitespace.py
# TESTS
offline-tests: # ran by jenkins
cmds:
- '{{.OFFLINETEST_BIN_RELEASE_DIR}}/offline-test --iso_data_path "./iso_data/{{.GAME}}" --game {{.GAME}} --fail-on-cmp'
offline-test-file:
cmds:
- '{{.OFFLINETEST_BIN_RELEASE_DIR}}/offline-test --iso_data_path "./iso_data/{{.GAME}}" --game {{.GAME}} --file {{.FILE}}'
offline-tests-fast:
cmds:
- '{{.OFFLINETEST_BIN_RELEASE_DIR}}/offline-test --iso_data_path "./iso_data/{{.GAME}}" --game {{.GAME}} --pretty-print --num_threads 32 --dump_current_output --fail-on-cmp'
# TODO - amalgamate offline-tests and this task, run twice if the previous step fails
update-ref-tests:
cmds:
- cmd: python ./scripts/tasks/delete-file-or-folder.py --path failures
- cmd: '{{.OFFLINETEST_BIN_RELEASE_DIR}}/offline-test --iso_data_path "./iso_data/{{.GAME}}" --game {{.GAME}} --pretty-print --num_threads 32 --dump_current_output --fail-on-cmp'
- cmd: python ./scripts/default-file-or-folder.py --path failures
- cmd: ./out/build/Release/bin/offline-test.exe "./iso_data/jak1" --dump-mode
ignore_error: true
- python ./scripts/update_decomp_reference.py ./failures ./test/decompiler/reference/ --game {{.GAME}}
- task: offline-tests-fast
update-ref-file:
cmds:
- cmd: python ./scripts/tasks/delete-file-or-folder.py --path failures
- cmd: '{{.OFFLINETEST_BIN_RELEASE_DIR}}/offline-test --iso_data_path "./iso_data/{{.GAME}}" --file {{.FILE}} --game {{.GAME}} --dump_current_output --fail-on-cmp'
ignore_error: true
- python ./scripts/update_decomp_reference.py ./failures ./test/decompiler/reference/ --game {{.GAME}}
- task: offline-test-file
type-test:
cmds:
- cmd: '{{.GOALCTEST_BIN_RELEASE_DIR}}/goalc-test --gtest_brief=0 --gtest_filter="*{{.TYPE_CONSISTENCY_TEST_FILTER}}*" --gtest_break_on_failure'
- python ./scripts/update_decomp_reference.py ./failures ./test/decompiler/reference/
- task: offline-tests
+2
View File
@@ -0,0 +1,2 @@
*
!.gitignore
+15 -71
View File
@@ -1,63 +1,17 @@
function(write_revision_h)
find_package(Git)
set(GIT_SHORT_SHA "")
set(GIT_TAG "")
if(NOT GIT_FOUND)
MESSAGE(STATUS "write_revision_h(): git was not found")
endif()
MESSAGE(STATUS "write_revision_h(): ${CMAKE_SOURCE_DIR}")
if (GIT_FOUND AND EXISTS ${CMAKE_SOURCE_DIR}/.git)
MESSAGE(STATUS "Git found, using it to get revision info")
EXECUTE_PROCESS(WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD
OUTPUT_VARIABLE GIT_SHORT_SHA
OUTPUT_STRIP_TRAILING_WHITESPACE)
EXECUTE_PROCESS(WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} tag --points-at HEAD
OUTPUT_VARIABLE GIT_TAG
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET)
endif()
file(WRITE ${CMAKE_CURRENT_SOURCE_DIR}/versions/revision.h "#define BUILT_TAG \"${GIT_TAG}\"\n#define BUILT_SHA \"${GIT_SHORT_SHA}\"\n")
endfunction()
write_revision_h()
add_library(common
SHARED
audio/audio_formats.cpp
cross_os_debug/xdbg.cpp
cross_sockets/XSocket.cpp
cross_sockets/XSocketClient.cpp
cross_sockets/XSocketServer.cpp
custom_data/pack_helpers.cpp
custom_data/TFrag3Data.cpp
dma/dma_copy.cpp
dma/dma.cpp
dma/gs.cpp
formatter/formatter_tree.cpp
formatter/formatter.cpp
formatter/formatter_tree.cpp
formatter/rules/formatting_rules.cpp
formatter/rules/rule_config.cpp
global_profiler/GlobalProfiler.cpp
cross_sockets/xsocket.cpp
goos/Interpreter.cpp
goos/Object.cpp
goos/ParseHelpers.cpp
goos/PrettyPrinter.cpp
goos/PrettyPrinter2.cpp
goos/Printer.cpp
goos/Reader.cpp
goos/TextDB.cpp
log/log.cpp
goos/ReplUtils.cpp
math/geometry.cpp
repl/config.cpp
repl/nrepl/ReplClient.cpp
repl/nrepl/ReplServer.cpp
repl/repl_wrapper.cpp
serialization/subtitles/subtitles_v1.cpp
serialization/subtitles/subtitles_v2.cpp
serialization/subtitles/subtitles.cpp
serialization/text/text_ser.cpp
sqlite/sqlite.cpp
texture/texture_slots.cpp
log/log.cpp
type_system/defenum.cpp
type_system/deftype.cpp
type_system/state.cpp
@@ -65,38 +19,28 @@ add_library(common
type_system/TypeFieldLookup.cpp
type_system/TypeSpec.cpp
type_system/TypeSystem.cpp
util/Assert.cpp
util/ast_util.cpp
util/BitUtils.cpp
util/compress.cpp
util/crc32.cpp
util/dgo_util.cpp
util/DgoReader.cpp
util/DgoWriter.cpp
util/dialogs.cpp
util/diff.cpp
util/FileUtil.cpp
util/FontUtils.cpp
util/FrameLimiter.cpp
util/json_util.cpp
util/os.cpp
util/print_float.cpp
util/read_iso_file.cpp
util/SimpleThreadGroup.cpp
util/string_util.cpp
util/term_util.cpp
util/Timer.cpp
util/unicode_util.cpp
util/gltf_util.cpp
versions/versions.cpp
util/print_float.cpp
)
target_link_libraries(common fmt lzokay replxx libzstd_static tree-sitter sqlite3 libtinyfiledialogs tiny_gltf)
target_link_libraries(common fmt lzokay replxx)
if(WIN32)
target_link_libraries(common wsock32 ws2_32 windowsapp)
elseif(APPLE)
# don't need anything special
target_link_libraries(common wsock32 ws2_32)
else()
target_link_libraries(common stdc++fs)
endif()
if(UNIX)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O2")
elseif(WIN32)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /O2")
endif()
install(TARGETS common)
+46 -105
View File
@@ -1,111 +1,68 @@
#include "audio_formats.h"
#include "common/log/log.h"
#include "common/util/BinaryWriter.h"
#include "fmt/core.h"
#include "third-party/fmt/core.h"
/*!
* Write a wave file from a vector of samples.
*/
void write_wave_file(const std::vector<s16>& left_samples,
const std::vector<s16>& right_samples,
s32 sample_rate,
const fs::path& name) {
void write_wave_file_mono(const std::vector<s16>& samples,
s32 sample_rate,
const std::string& name) {
WaveFileHeader header;
memcpy(header.chunk_id, "RIFF", 4);
header.chunk_size = 36 + ((left_samples.size() + right_samples.size()) * sizeof(s16));
header.chunk_size = 36 + samples.size() * sizeof(s16);
memcpy(header.format, "WAVE", 4);
// now the format
memcpy(header.subchunk1_id, "fmt ", 4);
header.subchunk1_size = 16;
header.aud_format = 1;
if (right_samples.empty()) {
header.num_channels = 1; // mono
} else {
header.num_channels = 2; // stereo
}
header.num_channels = 1; // mono
header.sample_rate = sample_rate;
header.byte_rate = sample_rate * header.num_channels * sizeof(s16);
header.block_align = header.num_channels * sizeof(s16);
header.bits_per_sample = 16;
memcpy(header.subchunk2_id, "data", 4);
header.subchunk2_size = (left_samples.size() + right_samples.size()) * sizeof(s16);
header.subchunk2_size = samples.size() * sizeof(s16);
BinaryWriter writer;
writer.add(header);
if (right_samples.empty()) {
for (const auto& sample : left_samples) {
writer.add(sample);
}
} else {
for (size_t i = 0; i < left_samples.size(); i++) {
writer.add(left_samples.at(i));
if (i < right_samples.size()) {
writer.add(right_samples.at(i));
} else {
writer.add(0);
}
}
for (auto& samp : samples) {
writer.add(samp);
}
writer.write_to_file(name);
}
std::pair<std::vector<s16>, std::vector<s16>> decode_adpcm(BinaryReader& reader,
const bool stereo) {
std::vector<s16> left_samples;
std::vector<s16> right_samples;
s32 left_sample_prev[2] = {0, 0};
s32 right_sample_prev[2] = {0, 0};
bool first_left = true;
bool first_right = true;
std::vector<s16> decode_adpcm(BinaryReader& reader) {
std::vector<s16> decoded_samples;
s32 sample_prev[2] = {0, 0};
constexpr s32 f1[5] = {0, 60, 115, 98, 122};
constexpr s32 f2[5] = {0, 0, -52, -55, -60};
[[maybe_unused]] int block_idx = 0;
// 16 byte blocks
int bytes_read = reader.get_seek(); // we've already read n bytes into the file
// Jak VAG's don't interleave the samples because of course they don't
// instead they are partitioned into contiguous 8kb (thats 8192 bytes) chunks
// alternating left/right
bool processing_left_chunk = true;
// We need to skip the vag header for each channel
if (first_left) {
reader.ffwd(48);
bytes_read += 48;
first_left = false;
}
int block_idx = 0;
while (true) {
if (!reader.bytes_left()) {
break;
}
if (stereo && bytes_read == 0x2000) {
// switch streams
processing_left_chunk = !processing_left_chunk;
bytes_read = 0;
// We need to skip the vag header for each channel
if (first_right) {
// skip the header
reader.ffwd(48);
bytes_read += 48;
first_right = false;
}
}
u8 shift_filter = reader.read<u8>();
u8 flags = reader.read<u8>();
u8 shift = shift_filter & 0b1111;
u8 filter = shift_filter >> 4;
u8 flags = reader.read<u8>();
(void)flags;
// removed assertions here (and that's probably why the audio doesn't sound right)
if (shift > 12) {
assert(false);
}
if (filter > 4) {
assert(false);
}
if (flags == 7) {
break;
}
u8 input_buffer[14];
@@ -123,42 +80,25 @@ std::pair<std::vector<s16>, std::vector<s16>> decode_adpcm(BinaryReader& reader,
s32 sample = (s32)(s16)(nibble << 12);
sample >>= shift;
sample += (sample_prev[0] * f1[filter] + sample_prev[1] * f2[filter] + 32) / 64;
if (!stereo || processing_left_chunk) {
sample += (left_sample_prev[0] * f1[filter] + left_sample_prev[1] * f2[filter] + 32) / 64;
if (sample > 0x7fff) {
sample = 0x7fff;
}
if (sample < -0x8000) {
sample = -0x8000;
}
left_sample_prev[1] = left_sample_prev[0];
left_sample_prev[0] = sample;
left_samples.push_back(sample);
} else {
sample += (right_sample_prev[0] * f1[filter] + right_sample_prev[1] * f2[filter] + 32) / 64;
if (sample > 0x7fff) {
sample = 0x7fff;
}
if (sample < -0x8000) {
sample = -0x8000;
}
right_sample_prev[1] = right_sample_prev[0];
right_sample_prev[0] = sample;
right_samples.push_back(sample);
if (sample > 0x7fff) {
sample = 0x7fff;
}
if (sample < -0x8000) {
sample = -0x8000;
}
sample_prev[1] = sample_prev[0];
sample_prev[0] = sample;
decoded_samples.push_back(sample);
}
bytes_read += 16;
block_idx++;
}
return {left_samples, right_samples};
return decoded_samples;
}
// I attempted to write an encoder below, which works, but has some limitations.
@@ -203,7 +143,7 @@ int get_shift_error(int shift, const s32* samples, bool /*debug*/) {
for (int sample_idx = 0; sample_idx < SAMPLES_PER_BLOCK; sample_idx++) {
int left_shift = 32 - (12 + 4 - shift);
ASSERT(left_shift >= 0);
assert(left_shift >= 0);
s32 sample_left = samples[sample_idx] << left_shift;
s32 sample_right = sample_left >> (32 - 4);
s32 sample_compressed = sample_right << (12 - shift);
@@ -314,7 +254,7 @@ void test_encode_adpcm(const std::vector<s16>& samples,
}
if (debug) {
lg::debug("Range: {}", max_sample - min_sample);
fmt::print("Range: {}\n", max_sample - min_sample);
}
// see how many bits we need and pick shift.
@@ -343,13 +283,14 @@ void test_encode_adpcm(const std::vector<s16>& samples,
if (filter_errors[best_filter] || best_filter != filter_debug[block_idx] ||
best_shift != shift_debug[block_idx]) {
lg::error("Block {} me {}, {} : answer {} {}: ERR {}", block_idx, best_filter, best_shift,
filter_debug[block_idx], shift_debug[block_idx], filter_errors[best_filter]);
lg::error("filter errors:");
fmt::print("Block {} me {}, {} : answer {} {}: ERR {}\n", block_idx, best_filter, best_shift,
filter_debug[block_idx], shift_debug[block_idx], filter_errors[best_filter]);
fmt::print("filter errors:\n");
for (int i = 0; i < 5; i++) {
lg::error(" [{}] {} {}", i, filter_errors[i], filter_shifts[i]);
fmt::print(" [{}] {} {}\n", i, filter_errors[i], filter_shifts[i]);
}
ASSERT_MSG(false, fmt::format("prev: {} {}", prev_block_samples[0], prev_block_samples[1]));
fmt::print("prev: {} {}\n", prev_block_samples[0], prev_block_samples[1]);
assert(false);
}
prev_block_samples[0] = samples.at(block_idx * 28 + 27);
+6 -8
View File
@@ -3,9 +3,8 @@
#include <string>
#include <vector>
#include "common/common_types.h"
#include "common/util/BinaryReader.h"
#include "common/util/FileUtil.h"
#include "common/common_types.h"
// The header data for a simple wave file
struct WaveFileHeader {
@@ -29,11 +28,10 @@ struct WaveFileHeader {
s32 subchunk2_size;
};
void write_wave_file(const std::vector<s16>& left_samples,
const std::vector<s16>& right_samples,
s32 sample_rate,
const fs::path& name);
void write_wave_file_mono(const std::vector<s16>& samples,
s32 sample_rate,
const std::string& name);
std::pair<std::vector<s16>, std::vector<s16>> decode_adpcm(BinaryReader& reader, const bool mono);
std::vector<s16> decode_adpcm(BinaryReader& reader);
std::vector<u8> encode_adpcm(const std::vector<s16>& samples);
std::vector<u8> encode_adpcm(const std::vector<s16>& samples);
+4 -7
View File
@@ -5,6 +5,9 @@
* Common Integer Types.
*/
#ifndef JAK1_COMMON_TYPES_H
#define JAK1_COMMON_TYPES_H
#include <cstdint>
using u8 = uint8_t;
@@ -19,18 +22,12 @@ using s64 = int64_t;
struct u128 {
union {
u64 du64[2];
s64 ds64[2];
u32 du32[4];
s32 ds32[4];
u16 du16[8];
s16 ds16[8];
u8 du8[16];
s8 ds8[16];
float f[4];
};
};
static_assert(sizeof(u128) == 16, "u128");
#if defined __linux || defined __linux__ || defined __APPLE__
#define OS_POSIX
#endif
#endif // JAK1_COMMON_TYPES_H
+28 -407
View File
@@ -4,32 +4,24 @@
* Nothing in here should hold state, that should all be managed in Debugger.
*/
#include "xdbg.h"
#include <cstring>
#include "common/goal_constants.h"
#include "common/util/Timer.h"
#include "fmt/core.h"
#include "third-party/fmt/core.h"
#include "xdbg.h"
#ifdef __linux
#include <fcntl.h>
#include <unistd.h>
#include <sys/prctl.h>
#include <sys/ptrace.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/ptrace.h>
#include <sys/prctl.h>
#include <sys/types.h>
#include <sys/user.h>
#include <sys/wait.h>
#include <fcntl.h>
#elif _WIN32
#define NOMINMAX
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <condition_variable>
#include <mutex>
#endif
namespace xdbg {
@@ -43,7 +35,9 @@ ThreadID::ThreadID(pid_t _id) : id(_id) {}
/*!
* In Linux, the string representation of a ThreadID is just the number printed in base 10
*/
ThreadID::ThreadID(const std::string& str) : id(std::stoi(str)) {}
ThreadID::ThreadID(const std::string& str) {
id = std::stoi(str);
}
std::string ThreadID::to_string() const {
return std::to_string(id);
@@ -100,13 +94,8 @@ bool check_stopped(const ThreadID& tid, SignalInfo* out) {
int status;
int rv = waitpid(tid.id, &status, WNOHANG);
if (rv < 0) {
if (errno == ECHILD) {
// the thing died.
out->kind = SignalInfo::DISAPPEARED;
return true;
}
printf("[Debugger] Failed to waitpid: %s.\n", strerror(errno));
// ASSERT(false); // todo, temp because I think we should never hit this.
// assert(false); // todo, temp because I think we should never hit this.
return false;
}
@@ -125,9 +114,6 @@ bool check_stopped(const ThreadID& tid, SignalInfo* out) {
case SIGTRAP:
out->kind = SignalInfo::BREAK;
break;
case SIGILL:
out->kind = SignalInfo::ILLEGAL_INSTR;
break;
default:
out->kind = SignalInfo::UNKNOWN;
@@ -303,421 +289,52 @@ bool cont_now(const ThreadID& tid) {
#elif _WIN32
ThreadID::ThreadID(DWORD _pid, DWORD _tid) : pid(_pid), tid(_tid) {}
ThreadID::ThreadID() {} // todo
ThreadID::ThreadID(const std::string& str) {
auto sep = str.find('-');
pid = std::stoi(str.substr(0, sep));
tid = std::stoi(str.substr(sep + 1));
// todo
}
std::string ThreadID::to_string() const {
return fmt::format("{}-{}", pid, tid);
// todo
return "0";
}
ThreadID get_current_thread_id() {
return ThreadID(GetCurrentProcessId(), GetCurrentThreadId());
// todo
return {};
}
void win_print_last_error(const std::string& msg) {
LPSTR lpMsgBuf;
FormatMessage(
FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&lpMsgBuf, 0, NULL);
printf("[Debugger] %s Win Err: %s", msg.c_str(), lpMsgBuf);
}
/*!
* Cross-thread things.
*/
int cont_status = -1; // hack? -1 = ignore; 0 = waiting for cont; 1 = cont'd, will resume
std::mutex m;
std::condition_variable cv;
bool attach_and_break(const ThreadID& tid) {
cont_status = -1;
if (!DebugActiveProcess(tid.pid)) {
win_print_last_error(fmt::format("DebugActiveProcess w/ TID {}", tid.to_string()));
return false;
}
// by default, windows debuggers will kill their debuggees on detach
DebugSetProcessKillOnExit(FALSE);
return true;
return false;
}
bool detach_and_resume(const ThreadID& tid) {
if (!DebugActiveProcessStop(tid.pid)) {
win_print_last_error("DebugActiveProcessStop");
return false;
}
return true;
return false;
}
void allow_debugging() {}
bool break_now(const ThreadID& tid) {
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, tid.pid);
auto result = DebugBreakProcess(hProc);
CloseHandle(hProc);
if (!result) {
win_print_last_error("DebugBreakProcess");
return false;
}
return true;
return false;
}
bool cont_now(const ThreadID& tid) {
if (cont_status != 0) {
return false;
}
{
std::unique_lock<std::mutex> lk(m);
cont_status = 1;
cv.notify_all();
}
return true;
return false;
}
DEBUG_EVENT debugEvent;
void ignore_debug_event() {
if (!ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE)) {
win_print_last_error("ContinueDebugEvent ignore_debug_event");
}
cont_status = -1;
}
const char* win32_exception_code_to_charp(DWORD exc) {
switch (exc) {
case EXCEPTION_ACCESS_VIOLATION:
return "EXCEPTION_ACCESS_VIOLATION";
case EXCEPTION_ARRAY_BOUNDS_EXCEEDED:
return "EXCEPTION_ARRAY_BOUNDS_EXCEEDED";
case EXCEPTION_BREAKPOINT:
return "EXCEPTION_BREAKPOINT";
case EXCEPTION_DATATYPE_MISALIGNMENT:
return "EXCEPTION_DATATYPE_MISALIGNMENT";
case EXCEPTION_FLT_DENORMAL_OPERAND:
return "EXCEPTION_FLT_DENORMAL_OPERAND";
case EXCEPTION_FLT_DIVIDE_BY_ZERO:
return "EXCEPTION_FLT_DIVIDE_BY_ZERO";
case EXCEPTION_FLT_INEXACT_RESULT:
return "EXCEPTION_FLT_INEXACT_RESULT";
case EXCEPTION_FLT_INVALID_OPERATION:
return "EXCEPTION_FLT_INVALID_OPERATION";
case EXCEPTION_FLT_OVERFLOW:
return "EXCEPTION_FLT_OVERFLOW";
case EXCEPTION_FLT_STACK_CHECK:
return "EXCEPTION_FLT_STACK_CHECK";
case EXCEPTION_FLT_UNDERFLOW:
return "EXCEPTION_FLT_UNDERFLOW";
case EXCEPTION_ILLEGAL_INSTRUCTION:
return "EXCEPTION_ILLEGAL_INSTRUCTION";
case EXCEPTION_IN_PAGE_ERROR:
return "EXCEPTION_IN_PAGE_ERROR";
case EXCEPTION_INT_DIVIDE_BY_ZERO:
return "EXCEPTION_INT_DIVIDE_BY_ZERO";
case EXCEPTION_INT_OVERFLOW:
return "EXCEPTION_INT_OVERFLOW";
case EXCEPTION_INVALID_DISPOSITION:
return "EXCEPTION_INVALID_DISPOSITION";
case EXCEPTION_NONCONTINUABLE_EXCEPTION:
return "EXCEPTION_NONCONTINUABLE_EXCEPTION";
case EXCEPTION_PRIV_INSTRUCTION:
return "EXCEPTION_PRIV_INSTRUCTION";
case EXCEPTION_SINGLE_STEP:
return "EXCEPTION_SINGLE_STEP";
case EXCEPTION_STACK_OVERFLOW:
return "EXCEPTION_STACK_OVERFLOW";
case STATUS_STACK_BUFFER_OVERRUN:
return "STATUS_STACK_BUFFER_OVERRUN";
case STATUS_HEAP_CORRUPTION:
return "STATUS_HEAP_CORRUPTION";
case STATUS_GUARD_PAGE_VIOLATION:
return "STATUS_GUARD_PAGE_VIOLATION";
default:
return "UNKNOWN (please contact developers)";
}
}
bool check_stopped(const ThreadID& tid, SignalInfo* out) {
{
std::unique_lock<std::mutex> lk(m);
if (cont_status != -1) {
cv.wait(lk, [&] { return cont_status == 1; });
if (!ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE)) {
win_print_last_error("ContinueDebugEvent check_stopped");
}
cont_status = -1;
}
}
if (WaitForDebugEvent(&debugEvent, INFINITE)) {
bool is_other = tid.pid != debugEvent.dwProcessId || tid.tid != debugEvent.dwThreadId;
cont_status = 0;
switch (debugEvent.dwDebugEventCode) {
case EXCEPTION_DEBUG_EVENT: // 1
{
auto exc = debugEvent.u.Exception.ExceptionRecord.ExceptionCode;
if (is_other) {
if (exc == EXCEPTION_BREAKPOINT) {
out->kind = SignalInfo::BREAK;
} else {
// ignore exceptions outside goal thread
ignore_debug_event();
}
} else {
switch (exc) {
case EXCEPTION_BREAKPOINT:
out->kind = SignalInfo::BREAK;
break;
case EXCEPTION_ILLEGAL_INSTRUCTION:
out->kind = SignalInfo::ILLEGAL_INSTR;
break;
case EXCEPTION_INT_DIVIDE_BY_ZERO:
case EXCEPTION_FLT_DENORMAL_OPERAND:
case EXCEPTION_FLT_DIVIDE_BY_ZERO:
case EXCEPTION_FLT_INEXACT_RESULT:
case EXCEPTION_FLT_INVALID_OPERATION:
case EXCEPTION_FLT_OVERFLOW:
case EXCEPTION_FLT_UNDERFLOW:
case EXCEPTION_FLT_STACK_CHECK:
out->kind = SignalInfo::MATH_EXCEPTION;
break;
case EXCEPTION_INT_OVERFLOW:
ignore_debug_event();
break;
default:
out->kind = SignalInfo::EXCEPTION;
out->msg = fmt::format("{} [0x{:X}]", win32_exception_code_to_charp(exc), exc);
break;
}
}
} break;
case CREATE_THREAD_DEBUG_EVENT: // 2
case CREATE_PROCESS_DEBUG_EVENT: // 3
case EXIT_THREAD_DEBUG_EVENT: // 4
case LOAD_DLL_DEBUG_EVENT: // 6
case UNLOAD_DLL_DEBUG_EVENT: // 7
case OUTPUT_DEBUG_STRING_EVENT: // 8
// don't care about these
// out->kind = SignalInfo::NOTHING;
ignore_debug_event();
break;
case EXIT_PROCESS_DEBUG_EVENT: // 5
case RIP_EVENT: // 9
out->kind = SignalInfo::DISAPPEARED;
break;
default:
printf("[Debugger] unhandled debug event %lu\n", debugEvent.dwDebugEventCode);
out->kind = SignalInfo::UNKNOWN;
break;
}
} else if (GetLastError() != 0x79) { // semaphore timeout error, irrelevant.
win_print_last_error("WaitForDebugEvent");
}
return cont_status != -1;
bool get_regs_now(const ThreadID& tid, Regs* out) {
return false;
}
bool open_memory(const ThreadID& tid, MemoryHandle* out) {
return true;
return false;
}
bool close_memory(const ThreadID& tid, MemoryHandle* handle) {
return true;
}
bool read_goal_memory(u8* dest_buffer,
int size,
u32 goal_addr,
const DebugContext& context,
const MemoryHandle& mem) {
SIZE_T read;
HANDLE hProc = OpenProcess(PROCESS_VM_READ, FALSE, context.tid.pid);
if (hProc == NULL) {
win_print_last_error("OpenProcess read_goal_memory");
return false;
}
auto result =
ReadProcessMemory(hProc, (LPCVOID)(context.base + goal_addr), dest_buffer, size, &read);
CloseHandle(hProc);
if (!result || read != size) {
win_print_last_error("ReadProcessMemory");
return false;
}
return true;
}
bool write_goal_memory(const u8* src_buffer,
int size,
u32 goal_addr,
const DebugContext& context,
const MemoryHandle& mem) {
SIZE_T written;
HANDLE hProc = OpenProcess(PROCESS_VM_WRITE, FALSE, context.tid.pid);
if (hProc == NULL) {
win_print_last_error("OpenProcess write_goal_memory");
return false;
}
auto result =
WriteProcessMemory(hProc, (LPVOID)(context.base + goal_addr), src_buffer, size, &written);
CloseHandle(hProc);
if (!result || written != size) {
win_print_last_error("WriteProcessMemory");
return false;
}
return true;
}
bool get_regs_now(const ThreadID& tid, Regs* out) {
CONTEXT context = {};
context.ContextFlags = CONTEXT_FULL;
HANDLE hThr = OpenThread(THREAD_GET_CONTEXT, FALSE, tid.tid);
if (hThr == NULL) {
win_print_last_error("OpenThread get_regs_now");
return false;
}
auto result = GetThreadContext(hThr, &context);
CloseHandle(hThr);
if (!result) {
win_print_last_error("GetThreadContext get_regs_now");
return false;
}
out->gprs[0] = context.Rax;
out->gprs[1] = context.Rcx;
out->gprs[2] = context.Rdx;
out->gprs[3] = context.Rbx;
out->gprs[4] = context.Rsp;
out->gprs[5] = context.Rbp;
out->gprs[6] = context.Rsi;
out->gprs[7] = context.Rdi;
out->gprs[8] = context.R8;
out->gprs[9] = context.R9;
out->gprs[10] = context.R10;
out->gprs[11] = context.R11;
out->gprs[12] = context.R12;
out->gprs[13] = context.R13;
out->gprs[14] = context.R14;
out->gprs[15] = context.R15;
out->rip = context.Rip;
// todo, get fprs.
return true;
}
bool set_regs_now(const ThreadID& tid, const Regs& out) {
CONTEXT context = {};
context.ContextFlags = CONTEXT_FULL;
HANDLE hThr = OpenThread(THREAD_GET_CONTEXT, FALSE, tid.tid);
if (hThr == NULL) {
win_print_last_error("OpenThread set_regs_now");
return false;
}
auto result = GetThreadContext(hThr, &context);
CloseHandle(hThr);
if (!result) {
win_print_last_error("GetThreadContext set_regs_now");
return false;
}
context.Rax = out.gprs[0];
context.Rcx = out.gprs[1];
context.Rdx = out.gprs[2];
context.Rbx = out.gprs[3];
context.Rsp = out.gprs[4];
context.Rbp = out.gprs[5];
context.Rsi = out.gprs[6];
context.Rdi = out.gprs[7];
context.R8 = out.gprs[8];
context.R9 = out.gprs[9];
context.R10 = out.gprs[10];
context.R11 = out.gprs[11];
context.R12 = out.gprs[12];
context.R13 = out.gprs[13];
context.R14 = out.gprs[14];
context.R15 = out.gprs[15];
context.Rip = out.rip;
hThr = OpenThread(THREAD_SET_CONTEXT, FALSE, tid.tid);
if (hThr == NULL) {
win_print_last_error("OpenThread set_regs_now set");
return false;
}
result = SetThreadContext(hThr, &context);
CloseHandle(hThr);
if (!result) {
win_print_last_error("SetThreadContext set_regs_now set");
return false;
}
// todo, set fprs.
return true;
}
#elif __APPLE__
ThreadID::ThreadID(const std::string& str) {}
std::string ThreadID::to_string() const {
return "invalid";
}
ThreadID get_current_thread_id() {
return ThreadID("not implemented on macOS");
}
bool attach_and_break(const ThreadID& tid);
void allow_debugging() {
printf("allow_debugging not implemented on macOS\n");
}
bool detach_and_resume(const ThreadID& tid) {
return false;
}
bool get_regs_now(const ThreadID& tid, Regs* out) {
return false;
}
bool set_regs_now(const ThreadID& tid, const Regs& in) {
return false;
}
bool break_now(const ThreadID& tid) {
return false;
}
bool cont_now(const ThreadID& tid) {
return false;
}
bool open_memory(const ThreadID& tid, MemoryHandle* out);
bool close_memory(const ThreadID& tid, MemoryHandle* handle) {
return false;
}
bool read_goal_memory(u8* dest_buffer,
int size,
u32 goal_addr,
@@ -737,6 +354,10 @@ bool write_goal_memory(const u8* src_buffer,
bool check_stopped(const ThreadID& tid, SignalInfo* out) {
return false;
}
bool set_regs_now(const ThreadID& tid, const Regs& out) {
return false;
}
#endif
const char* gpr_names[] = {"rax", "rcx", "rdx", "rbx", "rsp", "rbp", "rsi", "rdi",
+9 -20
View File
@@ -6,21 +6,18 @@
#pragma once
#include <cstdint>
#include <string>
#include <cstdint>
#include "common/common_types.h"
#ifdef __linux
#include <sys/types.h>
#elif _WIN32
#define NOMINMAX
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
// todo - windows includes
#endif
namespace xdbg {
#ifdef OS_POSIX
#ifdef __linux
/*!
* Identification for a thread.
@@ -43,16 +40,15 @@ struct MemoryHandle {
#elif _WIN32
struct ThreadID {
DWORD pid = 0;
DWORD tid = 0;
// todo - whatever windows uses to identify a thread
std::string to_string() const;
ThreadID(const std::string& str);
ThreadID(DWORD pid, DWORD tid);
ThreadID() = default;
ThreadID(); // todo - add id type here, like in linux version
};
struct MemoryHandle {};
struct MemoryHandle {
int a;
};
#endif
/*!
@@ -87,15 +83,8 @@ struct SignalInfo {
SEGFAULT, // access bad memory
BREAK, // hit a breakpoint or execute int3
MATH_EXCEPTION, // divide by zero
ILLEGAL_INSTR, // bad instruction
UNKNOWN, // some other signal that is unsupported
DISAPPEARED, // process disappeared (maybe killed by the user)
NOTHING, // nothing of importance. Windows sends many irrelevant (to us) events
EXCEPTION, // some unhandled exception
UNKNOWN // some other signal that is unsupported
} kind = UNKNOWN;
std::string msg;
};
// Functions
-191
View File
@@ -1,191 +0,0 @@
/*!
* @file xsocket.cpp
* Cross platform socket library used for the listener.
*/
// clang-format off
#include "common/common_types.h"
#ifdef OS_POSIX
#include <arpa/inet.h>
#include <netinet/tcp.h>
#include <sys/socket.h>
#include <unistd.h>
#elif _WIN32
#define WIN32_LEAN_AND_MEAN
#include <WinSock2.h>
#include <WS2tcpip.h>
#endif
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include "fmt/core.h"
#include "common/log/log.h"
// clang-format on
int open_socket(int af, int type, int protocol) {
#ifdef OS_POSIX
return socket(af, type, protocol);
#elif _WIN32
WSADATA wsaData = {0};
int iResult = 0;
// Initialize Winsock
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
lg::error("WSAStartup failed: {}", iResult);
return 1;
}
return socket(af, type, protocol);
#endif
}
int connect_socket(int socket, sockaddr* addr, int nameLen) {
int result = connect(socket, addr, nameLen);
if (result == -1) {
return -1;
}
return result;
}
#ifdef OS_POSIX
int accept_socket(int socket, sockaddr* addr, socklen_t* addrLen) {
return accept(socket, addr, addrLen);
}
int select_and_accept_socket(int socket, sockaddr* addr, socklen_t* addrLen, int microSeconds) {
struct timeval timeout;
timeout.tv_sec = 0;
timeout.tv_usec = microSeconds;
// Use select so it can timeout, accept on the returned socket if it is correct
fd_set read_sockets;
FD_ZERO(&read_sockets);
FD_SET(socket, &read_sockets);
auto activity = select(socket + 1, &read_sockets, NULL, NULL, &timeout);
if (activity > 0) {
return accept(socket, addr, addrLen);
}
return -1;
}
#endif
#ifdef _WIN32
int accept_socket(int socket, sockaddr* addr, int* addrLen) {
WSADATA wsaData = {0};
int iResult = 0;
// Initialize Winsock
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
lg::error("WSAStartup failed: {}", iResult);
return 1;
}
return accept(socket, addr, addrLen);
}
int select_and_accept_socket(int socket, sockaddr* addr, int* addrLen, int microSeconds) {
WSADATA wsaData = {0};
int iResult = 0;
// Initialize Winsock
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed: %d\n", iResult);
return 1;
}
struct timeval timeout;
timeout.tv_sec = 0;
timeout.tv_usec = microSeconds;
// Use select so it can timeout, accept on the returned socket if it is correct
fd_set read_sockets;
FD_ZERO(&read_sockets);
FD_SET(socket, &read_sockets);
auto activity = select(socket + 1, &read_sockets, NULL, NULL, &timeout);
if (activity > 0) {
return accept(socket, addr, addrLen);
}
return -1;
}
#endif
void close_socket(int sock) {
if (sock < 0) {
return;
}
#ifdef OS_POSIX
close(sock);
#elif _WIN32
closesocket(sock);
WSACleanup();
#endif
}
int set_socket_option(int socket, int level, int optname, const void* optval, int optlen) {
int ret = setsockopt(socket, level, optname, (const char*)optval, optlen);
if (ret < 0) {
lg::error("Failed to setsockopt({},{}, {}, _, _) - Error: {}", socket, level, optname,
strerror(errno));
}
#ifdef _WIN32
if (ret < 0) {
int err = WSAGetLastError();
lg::error("WSAGetLastError: {}", err);
}
#endif
return ret;
}
int set_socket_timeout(int socket, long microSeconds) {
#ifdef OS_POSIX
struct timeval timeout = {};
timeout.tv_sec = 0;
timeout.tv_usec = microSeconds;
int ret = setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, (struct timeval*)&timeout, sizeof(timeout));
if (ret < 0) {
printf("Failed to setsockopt(%d, %d, %d, _, _) - Error: %s\n", socket, SOL_SOCKET, SO_RCVTIMEO,
strerror(errno));
}
return ret;
#elif _WIN32
DWORD timeout = microSeconds / 1000; // milliseconds
// TODO - NOTE this might be bad / unreliable if the socket ends up being in a bad state
// select() instead should be used, will do so if ends up being an issue in practice
return set_socket_option(socket, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
#endif
}
int write_to_socket(int socket, const char* buf, int len) {
int bytes_wrote = 0;
#ifdef OS_POSIX
bytes_wrote = send(socket, buf, len, MSG_NOSIGNAL);
#elif _WIN32
bytes_wrote = send(socket, buf, len, 0);
#endif
if (bytes_wrote < 0) {
lg::error("[XSocket:{}] Error writing to socket", socket);
}
return bytes_wrote;
}
int read_from_socket(int socket, char* buf, int len) {
#ifdef OS_POSIX
return read(socket, buf, len);
#elif _WIN32
return recv(socket, buf, len, 0);
#endif
}
bool socket_timed_out() {
#ifdef OS_POSIX
return errno == EAGAIN;
#elif _WIN32
auto err = WSAGetLastError();
return err == WSAETIMEDOUT;
#endif
}
std::string address_to_string(const sockaddr_in& addr) {
char ip_str[INET_ADDRSTRLEN]; // Buffer to hold the IP address string
// Convert binary IP address to human-readable string using inet_ntop
if (inet_ntop(AF_INET, &(addr.sin_addr), ip_str, INET_ADDRSTRLEN) == nullptr) {
// Handle the error (e.g., log, throw an exception, etc.)
return "Error: Unable to convert IP address";
}
return ip_str;
}
-47
View File
@@ -1,47 +0,0 @@
#pragma once
/*!
* @file XSocket.h
* Cross platform socket library used for the listener.
*/
// clang-format off
#include "common/common_types.h"
#ifdef OS_POSIX
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sys/socket.h>
#include <unistd.h>
#elif _WIN32
#define NOMINMAX
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <WinSock2.h>
#endif
// clang-format on
#ifdef __linux
const int TCP_SOCKET_LEVEL = SOL_TCP;
#elif _WIN32
const int TCP_SOCKET_LEVEL = IPPROTO_TCP;
#elif __APPLE__
const int TCP_SOCKET_LEVEL = IPPROTO_TCP;
#endif
int open_socket(int af, int type, int protocol);
int connect_socket(int socket, sockaddr* addr, int nameLen);
#ifdef OS_POSIX
int accept_socket(int socket, sockaddr* addr, socklen_t* addrLen);
int select_and_accept_socket(int socket, sockaddr* addr, socklen_t* addrLen, int microSeconds);
#elif _WIN32
int accept_socket(int socket, sockaddr* addr, int* addrLen);
int select_and_accept_socket(int socket, sockaddr* addr, int* addrLen, int microSeconds);
#endif
void close_socket(int sock);
int set_socket_option(int socket, int level, int optname, const void* optval, int optlen);
int set_socket_timeout(int socket, long microSeconds);
int write_to_socket(int socket, const char* buf, int len);
int read_from_socket(int socket, char* buf, int len);
bool socket_timed_out();
std::string address_to_string(const sockaddr_in& addr);
-61
View File
@@ -1,61 +0,0 @@
#include "XSocketClient.h"
#include <string>
#include "common/cross_sockets/XSocket.h"
#include "common/log/log.h"
#include "fmt/core.h"
// clang-format off
#ifdef _WIN32
#define NOMINMAX
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <WinSock2.h>
#include <WS2tcpip.h>
#endif
// clang-format on
XSocketClient::XSocketClient(int _tcp_port) {
tcp_port = _tcp_port;
}
XSocketClient::~XSocketClient() {
disconnect();
client_socket = -1;
}
void XSocketClient::disconnect() {
close_socket(client_socket);
client_socket = -1;
}
bool XSocketClient::connect() {
// Open Socket
client_socket = open_socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (client_socket < 0) {
// TODO - log
disconnect();
return false;
}
addr.sin_family = AF_INET;
// Convert IP address from text to binary form using inet_pton()
if (inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr) <= 0) {
lg::error("inet_pton() failed");
return false;
}
addr.sin_port = htons(tcp_port);
// Connect to server
int result = connect_socket(client_socket, (sockaddr*)&addr, sizeof(addr));
if (result == -1) {
// TODO - log and close
disconnect();
return false;
}
return true;
}
-28
View File
@@ -1,28 +0,0 @@
#pragma once
#include <functional>
#include <mutex>
#include <thread>
#include "common/common_types.h"
#include "common/cross_sockets/XSocket.h"
/// @brief A cross platform generic socket client implementation
class XSocketClient {
public:
XSocketClient(int _tcp_port);
~XSocketClient();
XSocketClient(const XSocketClient&) = delete;
XSocketClient& operator=(const XSocketClient&) = delete;
bool connect();
void disconnect();
bool is_connected() { return client_socket != -1; }
protected:
int tcp_port;
struct sockaddr_in addr = {};
int client_socket = -1;
};
-107
View File
@@ -1,107 +0,0 @@
// clang-format off
#include "XSocketServer.h"
#include "common/cross_sockets/XSocket.h"
#include "common/common_types.h"
#include "fmt/core.h"
#ifdef _WIN32
#define NOMINMAX
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <WinSock2.h>
#include <WS2tcpip.h>
#endif
#include "common/log/log.h"
// clang-format on
XSocketServer::XSocketServer(std::function<bool()> shutdown_callback,
int _tcp_port,
int _buffer_size)
: want_exit_callback(std::move(shutdown_callback)) {
tcp_port = _tcp_port;
buffer.resize(_buffer_size);
}
XSocketServer::~XSocketServer() {
if (listening_socket >= 0) {
close_server_socket();
}
}
void XSocketServer::shutdown_server() {
// Close the listening and accepted socket socket
close_server_socket();
}
bool XSocketServer::init_server(bool failure_may_occur) {
listening_socket = open_socket(AF_INET, SOCK_STREAM, 0);
if (listening_socket < 0) {
listening_socket = -1;
return false;
}
int yes = 1;
#ifdef OS_POSIX
if (set_socket_option(listening_socket, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) < 0) {
close_server_socket();
return false;
}
// macOS doesn't support setting multiple options at once, so we have to do this separately.
if (set_socket_option(listening_socket, SOL_SOCKET, SO_REUSEPORT, &yes, sizeof(yes)) < 0) {
close_server_socket();
return false;
}
#elif _WIN32
if (set_socket_option(listening_socket, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, &yes, sizeof(yes)) < 0) {
close_server_socket();
return false;
};
#endif
if (set_socket_option(listening_socket, TCP_SOCKET_LEVEL, TCP_NODELAY, &yes, sizeof(yes)) < 0) {
close_server_socket();
return false;
}
if (set_socket_timeout(listening_socket, 100000) < 0) {
close_server_socket();
return false;
}
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(tcp_port);
if (bind(listening_socket, (sockaddr*)&addr, sizeof(addr)) < 0) {
if (failure_may_occur) {
lg::debug("[XSocketServer:{}] failed to bind", tcp_port);
} else {
lg::error("[XSocketServer:{}] failed to bind", tcp_port);
}
close_server_socket();
return false;
}
if (listen(listening_socket, 0) < 0) {
if (failure_may_occur) {
lg::debug("[XSocketServer:{}] failed to listen", tcp_port);
} else {
lg::error("[XSocketServer:{}] failed to listen", tcp_port);
}
close_server_socket();
return false;
}
server_initialized = true;
lg::debug("[XSocketServer:{}] initialized", tcp_port);
post_init();
return true;
}
void XSocketServer::close_server_socket() {
close_socket(listening_socket);
listening_socket = -1;
}
-39
View File
@@ -1,39 +0,0 @@
#pragma once
#include <functional>
#include <mutex>
#include <thread>
#include <vector>
#include "common/common_types.h"
#include "common/cross_sockets/XSocket.h"
/// @brief A cross platform generic socket server implementation
class XSocketServer {
public:
static constexpr int DEF_BUFFER_SIZE = 32 * 1024 * 1024;
XSocketServer(std::function<bool()> shutdown_callback,
int _tcp_port,
int _buffer_size = DEF_BUFFER_SIZE);
virtual ~XSocketServer();
XSocketServer(const XSocketServer&) = delete;
XSocketServer& operator=(const XSocketServer&) = delete;
bool init_server(bool failure_may_occur = false);
void shutdown_server();
void close_server_socket();
// Abstract methods -- use-case dependent
virtual void post_init() = 0;
protected:
int tcp_port;
struct sockaddr_in addr = {};
int listening_socket = -1;
std::vector<char> buffer;
bool server_initialized = false;
std::function<bool()> want_exit_callback;
};
+102
View File
@@ -0,0 +1,102 @@
/*!
* @file xsocket.cpp
* Cross platform socket library used for the listener.
*/
#ifdef __linux
#include <sys/socket.h>
#include <netinet/tcp.h>
#include <unistd.h>
#elif _WIN32
#define WIN32_LEAN_AND_MEAN
#include <WinSock2.h>
#include <WS2tcpip.h>
#endif
#include <errno.h>
#include <stdio.h>
#include <string.h>
int open_socket(int af, int type, int protocol) {
#ifdef __linux
return socket(af, type, protocol);
#elif _WIN32
WSADATA wsaData = {0};
int iResult = 0;
// Initialize Winsock
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed: %d\n", iResult);
return 1;
}
return socket(af, type, protocol);
#endif
}
void close_socket(int sock) {
if (sock < 0) {
return;
}
#ifdef __linux
close(sock);
#elif _WIN32
closesocket(sock);
WSACleanup();
#endif
}
int set_socket_option(int socket, int level, int optname, const void* optval, int optlen) {
int ret = setsockopt(socket, level, optname, (const char*)optval, optlen);
if (ret < 0) {
printf("Failed to setsockopt(%d, %d, %d, _, _) - Error: %s\n", socket, level, optname,
strerror(errno));
}
#ifdef _WIN32
if (ret < 0) {
int err = WSAGetLastError();
printf("WSAGetLastError: %d\n", err);
}
#endif
return ret;
}
int set_socket_timeout(int socket, long microSeconds) {
#ifdef __linux
struct timeval timeout = {};
timeout.tv_sec = 0;
timeout.tv_usec = microSeconds;
int ret = setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, (struct timeval*)&timeout, sizeof(timeout));
if (ret < 0) {
printf("Failed to setsockopt(%d, %d, %d, _, _) - Error: %s\n", socket, SOL_SOCKET, SO_RCVTIMEO,
strerror(errno));
}
return ret;
#elif _WIN32
DWORD timeout = microSeconds / 1000; // milliseconds
return set_socket_option(socket, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
#endif
}
int write_to_socket(int socket, const char* buf, int len) {
#ifdef __linux
return write(socket, buf, len);
#elif _WIN32
return send(socket, buf, len, 0);
#endif
}
int read_from_socket(int socket, char* buf, int len) {
#ifdef __linux
return read(socket, buf, len);
#elif _WIN32
return recv(socket, buf, len, 0);
#endif
}
bool socket_timed_out() {
#ifdef __linux
return errno == EAGAIN;
#elif _WIN32
auto err = WSAGetLastError();
return err == WSAETIMEDOUT;
#endif
}
+28
View File
@@ -0,0 +1,28 @@
#pragma once
/*!
* @file xsocket.h
* Cross platform socket library used for the listener.
*/
#ifdef __linux
#include <sys/socket.h>
#include <netinet/tcp.h>
#include <unistd.h>
#elif _WIN32
#include <WinSock2.h>
#endif
#ifdef __linux
const int TCP_SOCKET_LEVEL = SOL_TCP;
#elif _WIN32
const int TCP_SOCKET_LEVEL = IPPROTO_TCP;
#endif
int open_socket(int af, int type, int protocol);
void close_socket(int sock);
int set_socket_option(int socket, int level, int optname, const void* optval, int optlen);
int set_socket_timeout(int socket, long microSeconds);
int write_to_socket(int socket, const char* buf, int len);
int read_from_socket(int socket, char* buf, int len);
bool socket_timed_out();
-853
View File
@@ -1,853 +0,0 @@
#include "Tfrag3Data.h"
#include <algorithm>
#include <functional>
#ifndef __aarch64__
#include "xmmintrin.h"
#else
#include "third-party/sse2neon/sse2neon.h"
#endif
#include "common/log/log.h"
#include "common/util/Assert.h"
namespace tfrag3 {
void PackedTimeOfDay::serialize(Serializer& ser) {
ser.from_pod_vector(&data);
ser.from_ptr(&color_count);
}
void PackedTieVertices::serialize(Serializer& ser) {
ser.from_pod_vector(&color_indices);
ser.from_pod_vector(&matrices);
ser.from_pod_vector(&matrix_groups);
ser.from_pod_vector(&vertices);
}
void PackedShrubVertices::serialize(Serializer& ser) {
ser.from_pod_vector(&matrices);
ser.from_pod_vector(&instance_groups);
ser.from_pod_vector(&vertices);
ser.from_ptr(&total_vertex_count);
}
void StripDraw::serialize(Serializer& ser) {
ser.from_ptr(&mode);
ser.from_ptr(&tree_tex_id);
ser.from_pod_vector(&runs);
ser.from_pod_vector(&plain_indices);
ser.from_pod_vector(&vis_groups);
ser.from_ptr(&num_triangles);
}
void ShrubDraw::serialize(Serializer& ser) {
ser.from_ptr(&mode);
ser.from_ptr(&tree_tex_id);
ser.from_ptr(&num_triangles);
ser.from_ptr(&first_index_index);
ser.from_ptr(&num_indices);
ser.from_ptr(&proto_idx);
}
void InstancedStripDraw::serialize(Serializer& ser) {
ser.from_ptr(&mode);
ser.from_ptr(&tree_tex_id);
ser.from_pod_vector(&vertex_index_stream);
ser.from_pod_vector(&instance_groups);
ser.from_ptr(&num_triangles);
}
void TieWindInstance::serialize(Serializer& ser) {
ser.from_ptr(&matrix);
ser.from_ptr(&wind_idx);
ser.from_ptr(&stiffness);
}
void TfragTree::serialize(Serializer& ser) {
ser.from_ptr(&kind);
if (ser.is_saving()) {
ser.save<size_t>(draws.size());
} else {
draws.resize(ser.load<size_t>());
}
for (auto& draw : draws) {
draw.serialize(ser);
}
ser.from_pod_vector(&packed_vertices.vertices);
ser.from_pod_vector(&packed_vertices.cluster_origins);
colors.serialize(ser);
bvh.serialize(ser);
ser.from_ptr(&use_strips);
}
math::Vector3f vopmula(math::Vector3f a, math::Vector3f b) {
return math::Vector3f(a.y() * b.z(), a.z() * b.x(), a.x() * b.y());
}
math::Vector3f vopmsub(math::Vector3f acc, math::Vector3f a, math::Vector3f b) {
return acc - vopmula(a, b);
}
/*!
* Compute the normal transformation for a TIE from the TIE matrix.
* Note that this isn't identical to the original game - we're missing the vf14 scaling factor
* For now, I just set this to 1, then normalize in the shader. Though I think we could avoid
* this by figuring out the value of vf14 here (I am just too lazy right now).
*/
std::array<math::Vector3f, 3> tie_normal_transform_v2(const std::array<math::Vector4f, 4>& m) {
// let:
// vf10, vf11, vf12, vf13 be the input matrix m
std::array<math::Vector3f, 3> result;
auto& vf10 = m[0];
auto& vf11 = m[1];
// auto& vf12 = m[2];
// lui t6, 16256
// mtc1 f1, t6 ;; 1.0
//
// qmfc2.i s1, vf10
// mtc1 f12, s1
float f12 = vf10.x();
// dsra32 s2, s1, 0
// mtc1 f13, s2
float f13 = vf10.y();
// pextuw s2, r0, s2
// mtc1 f14, s2
float f14 = vf10.z();
// mula.s f12, f12
// madda.s f13, f13
// madd.s f15, f14, f14
float f15 = f12 * f12 + f13 * f13 + f14 * f14;
float scale = 1.f / sqrtf(f15);
// rsqrt.s f15, f1, f15
// mfc1 s1, f15
// qmtc2.i vf14, s1
// vmulx.xyz vf16, vf10, vf14
// vmulx.xyz vf16, vf10, vf14
math::Vector3f vf16 = vf10.xyz() * scale;
// vopmula.xyz acc, vf11, vf16
math::Vector3f acc = vopmula(vf11.xyz(), vf16);
// vopmsub.xyz vf17, vf16, vf11
math::Vector3f vf17 = vopmsub(acc, vf16, vf11.xyz());
// vopmula.xyz acc, vf16, vf17
acc = vopmula(vf16, vf17);
// vopmsub.xyz vf17, vf17, vf16
vf17 = vopmsub(acc, vf17, vf16);
// vmul.xyz vf14, vf17, vf17
math::Vector3f vf14 = vf17.elementwise_multiply(vf17);
// vmulax.w acc, vf0, vf14
// vmadday.w acc, vf0, vf14
// vmaddz.w vf14, vf0, vf14
float sum = vf14.x() + vf14.y() + vf14.z();
// vrsqrt Q, vf0.w, vf14.w
float Q = 1.f / std::sqrt(sum);
// vmulax.xyzw acc, vf24, vf16
// vmadday.xyzw acc, vf25, vf16
// vmaddz.xyzw vf10, vf26, vf16
// vf10 = vf16; // assume cam is identity here.
result[0] = vf16;
// vwaitq
// vmulq.xyz vf17, vf17, Q
vf17 *= Q;
// vopmula.xyz acc, vf16, vf17
acc = vopmula(vf16, vf17);
// vopmsub.xyz vf18, vf17, vf16
math::Vector3f vf18 = vopmsub(acc, vf17, vf16);
// vmulax.xyzw acc, vf24, vf17
// vmadday.xyzw acc, vf25, vf17
// vmaddz.xyzw vf11, vf26, vf17
result[1] = vf17;
// vmulax.xyzw acc, vf24, vf18
// vmadday.xyzw acc, vf25, vf18
// vmaddz.xyzw vf12, vf26, vf18
result[2] = vf18;
return result;
//
// sqc2 vf10, -112(t8)
// sqc2 vf11, -96(t8)
// sqc2 vf12, -80(t8)
}
u32 pack_to_gl_normal(s16 nx, s16 ny, s16 nz) {
ASSERT(nx >= -512 && nx <= 511);
ASSERT(ny >= -512 && ny <= 511);
ASSERT(nz >= -512 && nz <= 511);
return (nx & 0x3ff) | ((ny & 0x3ff) << 10) | ((nz & 0x3ff) << 20);
}
/*!
* Unpack tie normal by transforming and converting to s16 for OpenGL.
*/
u32 unpack_tie_normal(const std::array<math::Vector3f, 3>& mat, s8 nx, s8 ny, s8 nz) {
// rotate the normal
math::Vector3f nrm = math::Vector3f::zero();
nrm += mat[0] * nx;
nrm += mat[1] * ny;
nrm += mat[2] * nz;
// convert to s16 for OpenGL renderer
// nrm /= 0x100; // number from EE asm
// nrm *= 0x200; // for normalized s10 -> float conversion by OpenGL.
nrm *= 2; // for normalized s10 -> float conversion by OpenGL.
auto as_int = nrm.cast<s16>();
return pack_to_gl_normal(as_int.x(), as_int.y(), as_int.z());
}
/*
void tie_normal_v3(__m128* out, const std::array<math::Vector4f, 4>& in) {
math::Vector3f x_row = in[0].xyz();
math::Vector3f y_row = in[1].xyz();
math::Vector3f z_row = in[2].xyz();
x_row.normalize();
y_row = x_row.cross(y_row.cross(x_row)).normalized();
z_row = x_row.cross(y_row);
out[0] = _mm_setr_ps(x_row[0], x_row[1], x_row[2], 0);
out[1] = _mm_setr_ps(y_row[0], y_row[1], y_row[2], 0);
out[2] = _mm_setr_ps(z_row[0], z_row[1], z_row[2], 0);
}
*/
void TieTree::unpack() {
unpacked.vertices.resize(packed_vertices.color_indices.size());
size_t i = 0;
for (const auto& grp : packed_vertices.matrix_groups) {
if (grp.matrix_idx == -1) {
for (u32 src_idx = grp.start_vert; src_idx < grp.end_vert; src_idx++) {
auto& vtx = unpacked.vertices[i];
vtx.color_index = packed_vertices.color_indices[i];
const auto& proto_vtx = packed_vertices.vertices[src_idx];
vtx.x = proto_vtx.x;
vtx.y = proto_vtx.y;
vtx.z = proto_vtx.z;
vtx.s = proto_vtx.s;
vtx.t = proto_vtx.t;
vtx.nor = pack_to_gl_normal(proto_vtx.nx << 1, proto_vtx.ny << 1, proto_vtx.nz << 1);
vtx.r = proto_vtx.r;
vtx.g = proto_vtx.g;
vtx.b = proto_vtx.b;
vtx.a = proto_vtx.a;
i++;
}
} else {
const auto& mat = packed_vertices.matrices[grp.matrix_idx];
__m128 mat0 = _mm_loadu_ps(mat[0].data());
__m128 mat1 = _mm_loadu_ps(mat[1].data());
__m128 mat2 = _mm_loadu_ps(mat[2].data());
__m128 mat3 = _mm_loadu_ps(mat[3].data());
if (grp.has_normals) {
auto nmat = tie_normal_transform_v2(mat);
for (u32 src_idx = grp.start_vert; src_idx < grp.end_vert; src_idx++) {
auto& vtx = unpacked.vertices[i];
vtx.color_index = packed_vertices.color_indices[i];
const auto& proto_vtx = packed_vertices.vertices[src_idx];
// auto temp = mat[0] * proto_vtx.x + mat[1] * proto_vtx.y + mat[2] * proto_vtx.z +
// mat[3];
__m128 transformed = mat3;
transformed = _mm_add_ps(transformed, _mm_mul_ps(_mm_set1_ps(proto_vtx.x), mat0));
transformed = _mm_add_ps(transformed, _mm_mul_ps(_mm_set1_ps(proto_vtx.y), mat1));
transformed = _mm_add_ps(transformed, _mm_mul_ps(_mm_set1_ps(proto_vtx.z), mat2));
_mm_storeu_ps(&vtx.x, transformed);
vtx.s = proto_vtx.s;
vtx.t = proto_vtx.t;
vtx.nor = unpack_tie_normal(nmat, proto_vtx.nx, proto_vtx.ny, proto_vtx.nz);
vtx.r = proto_vtx.r;
vtx.g = proto_vtx.g;
vtx.b = proto_vtx.b;
vtx.a = proto_vtx.a;
i++;
}
} else {
for (u32 src_idx = grp.start_vert; src_idx < grp.end_vert; src_idx++) {
auto& vtx = unpacked.vertices[i];
vtx.color_index = packed_vertices.color_indices[i];
const auto& proto_vtx = packed_vertices.vertices[src_idx];
__m128 transformed = mat3;
transformed = _mm_add_ps(transformed, _mm_mul_ps(_mm_set1_ps(proto_vtx.x), mat0));
transformed = _mm_add_ps(transformed, _mm_mul_ps(_mm_set1_ps(proto_vtx.y), mat1));
transformed = _mm_add_ps(transformed, _mm_mul_ps(_mm_set1_ps(proto_vtx.z), mat2));
_mm_storeu_ps(&vtx.x, transformed);
vtx.s = proto_vtx.s;
vtx.t = proto_vtx.t;
vtx.nor = 0;
vtx.r = proto_vtx.r;
vtx.g = proto_vtx.g;
vtx.b = proto_vtx.b;
vtx.a = proto_vtx.a;
i++;
}
}
}
}
for (auto& draw : static_draws) {
draw.unpacked.idx_of_first_idx_in_full_buffer = unpacked.indices.size();
ASSERT(draw.plain_indices.empty());
for (auto& run : draw.runs) {
for (u32 ri = 0; ri < run.length; ri++) {
unpacked.indices.push_back(run.vertex0 + ri);
}
unpacked.indices.push_back(UINT32_MAX);
}
}
}
void ShrubTree::unpack() {
unpacked.vertices.resize(packed_vertices.total_vertex_count);
size_t i = 0;
for (const auto& grp : packed_vertices.instance_groups) {
const auto& mat = packed_vertices.matrices[grp.matrix_idx];
for (u32 src_idx = grp.start_vert; src_idx < grp.end_vert; src_idx++) {
auto& vtx = unpacked.vertices[i];
vtx.color_index = grp.color_index;
const auto& proto_vtx = packed_vertices.vertices[src_idx];
auto temp = mat[0] * proto_vtx.x + mat[1] * proto_vtx.y + mat[2] * proto_vtx.z + mat[3];
vtx.x = temp.x();
vtx.y = temp.y();
vtx.z = temp.z();
vtx.s = proto_vtx.s;
vtx.t = proto_vtx.t;
memcpy(vtx.rgba_base, proto_vtx.rgba, 3);
i++;
}
}
ASSERT(i == unpacked.vertices.size());
}
void TfragTree::unpack() {
unpacked.vertices.resize(packed_vertices.vertices.size());
for (size_t i = 0; i < unpacked.vertices.size(); i++) {
auto& o = unpacked.vertices[i];
auto& in = packed_vertices.vertices[i];
auto& cluster = packed_vertices.cluster_origins.at(in.cluster_idx);
constexpr float kClusterSize = 4096 * 40; // 100 in-game meters
constexpr float kMasterOffset = 12000 * 4096;
constexpr float rescale = kClusterSize / UINT16_MAX;
float cx = -kMasterOffset + kClusterSize * cluster.x();
float cy = -kMasterOffset + kClusterSize * cluster.y();
float cz = -kMasterOffset + kClusterSize * cluster.z();
o.x = cx + in.xoff * rescale;
o.y = cy + in.yoff * rescale;
o.z = cz + in.zoff * rescale;
o.s = in.s / (1024.f);
o.t = in.t / (1024.f);
o.color_index = in.color_index;
}
for (auto& draw : draws) {
draw.unpacked.idx_of_first_idx_in_full_buffer = unpacked.indices.size();
for (auto& run : draw.runs) {
for (u32 ri = 0; ri < run.length; ri++) {
unpacked.indices.push_back(run.vertex0 + ri);
}
if (use_strips) {
unpacked.indices.push_back(UINT32_MAX);
}
}
unpacked.indices.insert(unpacked.indices.end(), draw.plain_indices.begin(),
draw.plain_indices.end());
}
}
void TieTree::serialize(Serializer& ser) {
if (ser.is_saving()) {
ser.save<size_t>(static_draws.size());
} else {
static_draws.resize(ser.load<size_t>());
}
for (auto& draw : static_draws) {
draw.serialize(ser);
}
ser.from_ptr(&category_draw_indices);
if (ser.is_saving()) {
ser.save<size_t>(instanced_wind_draws.size());
} else {
instanced_wind_draws.resize(ser.load<size_t>());
}
for (auto& draw : instanced_wind_draws) {
draw.serialize(ser);
}
if (ser.is_saving()) {
ser.save<size_t>(wind_instance_info.size());
} else {
wind_instance_info.resize(ser.load<size_t>());
}
for (auto& inst : wind_instance_info) {
inst.serialize(ser);
}
packed_vertices.serialize(ser);
colors.serialize(ser);
bvh.serialize(ser);
ser.from_ptr(&has_per_proto_visibility_toggle);
ser.from_string_vector(&proto_names);
}
void ShrubTree::serialize(Serializer& ser) {
time_of_day_colors.serialize(ser);
ser.from_pod_vector(&indices);
packed_vertices.serialize(ser);
if (ser.is_saving()) {
ser.save<size_t>(static_draws.size());
} else {
static_draws.resize(ser.load<size_t>());
}
for (auto& draw : static_draws) {
draw.serialize(ser);
}
ser.from_ptr(&has_per_proto_visibility_toggle);
ser.from_string_vector(&proto_names);
}
void HfragmentBucket::serialize(Serializer& ser) {
ser.from_pod_vector(&corners);
ser.from_ptr(&montage_table);
}
void Hfragment::serialize(Serializer& ser) {
ser.from_pod_vector(&vertices);
ser.from_pod_vector(&indices);
ser.from_pod_vector(&corners);
if (ser.is_saving()) {
ser.save<size_t>(buckets.size());
} else {
buckets.resize(ser.load<size_t>());
}
for (auto& x : buckets) {
x.serialize(ser);
}
time_of_day_colors.serialize(ser);
ser.from_ptr(&wang_tree_tex_id);
ser.from_ptr(&draw_mode);
ser.from_ptr(&occlusion_offset);
}
void BVH::serialize(Serializer& ser) {
ser.from_ptr(&first_leaf_node);
ser.from_ptr(&last_leaf_node);
ser.from_ptr(&first_root);
ser.from_ptr(&num_roots);
ser.from_ptr(&only_children);
ser.from_pod_vector(&vis_nodes);
}
void Texture::serialize(Serializer& ser) {
ser.from_ptr(&w);
ser.from_ptr(&h);
ser.from_ptr(&combo_id);
ser.from_pod_vector(&data);
ser.from_str(&debug_name);
ser.from_str(&debug_tpage_name);
ser.from_ptr(&load_to_pool);
}
void IndexTexture::serialize(Serializer& ser) {
ser.from_ptr(&w);
ser.from_ptr(&h);
ser.from_ptr(&combo_id);
ser.from_pod_vector(&index_data);
ser.from_ptr(&color_table);
ser.from_str(&name);
ser.from_str(&tpage_name);
ser.from_string_vector(&level_names);
}
void CollisionMesh::serialize(Serializer& ser) {
ser.from_pod_vector(&vertices);
}
void MercDraw::serialize(Serializer& ser) {
ser.from_ptr(&mode);
ser.from_ptr(&tree_tex_id);
ser.from_ptr(&eye_id);
ser.from_ptr(&first_index);
ser.from_ptr(&index_count);
ser.from_ptr(&num_triangles);
ser.from_ptr(&no_strip);
}
void Blerc::serialize(Serializer& ser) {
ser.from_pod_vector(&float_data);
ser.from_pod_vector(&int_data);
}
void MercModifiableDrawGroup::serialize(Serializer& ser) {
if (ser.is_saving()) {
ser.save<size_t>(mod_draw.size());
} else {
mod_draw.resize(ser.load<size_t>());
}
for (auto& draw : mod_draw) {
draw.serialize(ser);
}
if (ser.is_saving()) {
ser.save<size_t>(fix_draw.size());
} else {
fix_draw.resize(ser.load<size_t>());
}
for (auto& draw : fix_draw) {
draw.serialize(ser);
}
ser.from_pod_vector(&vertices);
ser.from_pod_vector(&vertex_lump4_addr);
ser.from_pod_vector(&fragment_mask);
ser.from_ptr(&expect_vidx_end);
blerc.serialize(ser);
}
void MercEffect::serialize(Serializer& ser) {
if (ser.is_saving()) {
ser.save<size_t>(all_draws.size());
} else {
all_draws.resize(ser.load<size_t>());
}
for (auto& draw : all_draws) {
draw.serialize(ser);
}
mod.serialize(ser);
ser.from_ptr(&envmap_mode);
ser.from_ptr(&envmap_texture);
ser.from_ptr(&has_envmap);
ser.from_ptr(&has_mod_draw);
}
void MercModel::serialize(Serializer& ser) {
ser.from_str(&name);
if (ser.is_saving()) {
ser.save<size_t>(effects.size());
} else {
effects.resize(ser.load<size_t>());
}
for (auto& effect : effects) {
effect.serialize(ser);
}
ser.from_ptr(&max_draws);
ser.from_ptr(&max_bones);
ser.from_ptr(&st_vif_add);
ser.from_ptr(&xyz_scale);
ser.from_ptr(&st_magic);
}
void MercModelGroup::serialize(Serializer& ser) {
if (ser.is_saving()) {
ser.save<size_t>(models.size());
} else {
models.resize(ser.load<size_t>());
}
for (auto& model : models) {
model.serialize(ser);
}
ser.from_pod_vector(&indices);
ser.from_pod_vector(&vertices);
}
void Level::serialize(Serializer& ser) {
ser.from_ptr(&version);
if (ser.is_loading() && version != TFRAG3_VERSION) {
ASSERT_MSG(false, fmt::format("version mismatch when loading tfrag3 data. Got {}, expected {}, "
"did you forget to re-decompile?",
version, TFRAG3_VERSION));
}
ser.from_str(&level_name);
if (ser.is_saving()) {
ser.save<size_t>(textures.size());
} else {
textures.resize(ser.load<size_t>());
}
for (auto& tex : textures) {
tex.serialize(ser);
}
if (ser.is_saving()) {
ser.save<size_t>(index_textures.size());
} else {
index_textures.resize(ser.load<size_t>());
}
for (auto& tex : index_textures) {
tex.serialize(ser);
}
for (int geom = 0; geom < 3; ++geom) {
if (ser.is_saving()) {
ser.save<size_t>(tfrag_trees[geom].size());
} else {
tfrag_trees[geom].resize(ser.load<size_t>());
}
for (auto& tree : tfrag_trees[geom]) {
tree.serialize(ser);
}
}
for (int geom = 0; geom < 4; ++geom) {
if (ser.is_saving()) {
ser.save<size_t>(tie_trees[geom].size());
} else {
tie_trees[geom].resize(ser.load<size_t>());
}
for (auto& tree : tie_trees[geom]) {
tree.serialize(ser);
}
}
if (ser.is_saving()) {
ser.save<size_t>(shrub_trees.size());
} else {
shrub_trees.resize(ser.load<size_t>());
}
for (auto& tree : shrub_trees) {
tree.serialize(ser);
}
hfrag.serialize(ser);
collision.serialize(ser);
merc_data.serialize(ser);
ser.from_ptr(&version2);
if (ser.is_loading() && version2 != TFRAG3_VERSION) {
ASSERT_MSG(false, fmt::format(
"version mismatch when loading tfrag3 data (at end). Got {}, expected {}",
version2, TFRAG3_VERSION));
}
}
void MercModifiableDrawGroup::memory_usage(MemoryUsageTracker* tracker) const {
tracker->add(MemoryUsageCategory::MERC_MOD_VERT, sizeof(MercVertex) * vertices.size());
tracker->add(MemoryUsageCategory::MERC_MOD_DRAW_1, sizeof(MercDraw) * fix_draw.size());
tracker->add(MemoryUsageCategory::MERC_MOD_DRAW_2, sizeof(MercDraw) * mod_draw.size());
tracker->add(MemoryUsageCategory::MERC_MOD_TABLE, sizeof(u16) * vertex_lump4_addr.size());
tracker->add(MemoryUsageCategory::BLERC, sizeof(BlercFloatData) * blerc.float_data.size());
tracker->add(MemoryUsageCategory::BLERC, sizeof(u32) * blerc.int_data.size());
}
void MercEffect::memory_usage(MemoryUsageTracker* tracker) const {
tracker->add(MemoryUsageCategory::MERC_DRAW, sizeof(MercDraw) * all_draws.size());
mod.memory_usage(tracker);
}
void MercModel::memory_usage(MemoryUsageTracker* tracker) const {
for (auto& effect : effects) {
effect.memory_usage(tracker);
}
}
void MercModelGroup::memory_usage(MemoryUsageTracker* tracker) const {
tracker->add(MemoryUsageCategory::MERC_VERT, sizeof(MercVertex) * vertices.size());
tracker->add(MemoryUsageCategory::MERC_INDEX, sizeof(u32) * indices.size());
for (auto& model : models) {
model.memory_usage(tracker);
}
}
void CollisionMesh::memory_usage(MemoryUsageTracker* tracker) const {
tracker->add(MemoryUsageCategory::COLLISION, sizeof(Vertex) * vertices.size());
}
void PackedShrubVertices::memory_usage(MemoryUsageTracker* tracker) const {
tracker->add(MemoryUsageCategory::SHRUB_VERT, 64 * matrices.size());
tracker->add(MemoryUsageCategory::SHRUB_VERT, sizeof(InstanceGroup) * instance_groups.size());
tracker->add(MemoryUsageCategory::SHRUB_VERT, sizeof(Vertex) * vertices.size());
}
void ShrubTree::memory_usage(MemoryUsageTracker* tracker) const {
tracker->add(MemoryUsageCategory::SHRUB_TIME_OF_DAY, sizeof(u8) * time_of_day_colors.data.size());
packed_vertices.memory_usage(tracker);
tracker->add(MemoryUsageCategory::SHRUB_DRAW, sizeof(ShrubDraw) * static_draws.size());
tracker->add(MemoryUsageCategory::SHRUB_IND, sizeof(u32) * indices.size());
}
void InstancedStripDraw::memory_usage(MemoryUsageTracker* tracker) const {
tracker->add(MemoryUsageCategory::TIE_INST_INDEX, sizeof(u32) * vertex_index_stream.size());
tracker->add(MemoryUsageCategory::TIE_INST_VIS, sizeof(InstanceGroup) * instance_groups.size());
}
void PackedTieVertices::memory_usage(MemoryUsageTracker* tracker) const {
tracker->add(MemoryUsageCategory::TIE_CIDX, sizeof(u16) * color_indices.size());
tracker->add(MemoryUsageCategory::TIE_MATRICES, 64 * matrices.size());
tracker->add(MemoryUsageCategory::TIE_GRPS, sizeof(MatrixGroup) * matrix_groups.size());
tracker->add(MemoryUsageCategory::TIE_VERTS, sizeof(Vertex) * vertices.size());
}
void TieTree::memory_usage(MemoryUsageTracker* tracker) const {
tracker->add(MemoryUsageCategory::TIE_BVH, sizeof(VisNode) * bvh.vis_nodes.size());
for (auto& draw : static_draws) {
tracker->add(MemoryUsageCategory::TIE_DEINST_INDEX,
draw.runs.size() * sizeof(StripDraw::VertexRun));
tracker->add(MemoryUsageCategory::TIE_DEINST_INDEX, draw.plain_indices.size() * sizeof(u32));
tracker->add(MemoryUsageCategory::TIE_DEINST_VIS,
draw.vis_groups.size() * sizeof(StripDraw::VisGroup));
}
packed_vertices.memory_usage(tracker);
tracker->add(MemoryUsageCategory::TIE_TIME_OF_DAY, sizeof(u8) * colors.data.size());
for (auto& draw : instanced_wind_draws) {
draw.memory_usage(tracker);
}
tracker->add(MemoryUsageCategory::TIE_WIND_INSTANCE_INFO,
sizeof(TieWindInstance) * wind_instance_info.size());
}
void PackedTfragVertices::memory_usage(MemoryUsageTracker* tracker) const {
tracker->add(MemoryUsageCategory::TFRAG_VERTS,
sizeof(PackedTfragVertices::Vertex) * vertices.size());
tracker->add(MemoryUsageCategory::TFRAG_CLUSTER,
sizeof(math::Vector<u16, 3>) * cluster_origins.size());
}
void TfragTree::memory_usage(MemoryUsageTracker* tracker) const {
for (auto& draw : draws) {
tracker->add(MemoryUsageCategory::TFRAG_INDEX, draw.runs.size() * sizeof(StripDraw::VertexRun));
tracker->add(MemoryUsageCategory::TFRAG_INDEX, draw.plain_indices.size() * sizeof(u32));
tracker->add(MemoryUsageCategory::TFRAG_VIS,
draw.vis_groups.size() * sizeof(StripDraw::VisGroup));
}
packed_vertices.memory_usage(tracker);
tracker->add(MemoryUsageCategory::TFRAG_TIME_OF_DAY, sizeof(u8) * colors.data.size());
tracker->add(MemoryUsageCategory::TFRAG_BVH, sizeof(VisNode) * bvh.vis_nodes.size());
}
void Texture::memory_usage(MemoryUsageTracker* tracker) const {
tracker->add(MemoryUsageCategory::TEXTURE, data.size() * sizeof(u32));
}
void IndexTexture::memory_usage(MemoryUsageTracker* tracker) const {
tracker->add(MemoryUsageCategory::SPECIAL_TEXTURE, index_data.size());
tracker->add(MemoryUsageCategory::SPECIAL_TEXTURE, 256 * 4); // clut
}
void Hfragment::memory_usage(tfrag3::MemoryUsageTracker* tracker) const {
tracker->add(MemoryUsageCategory::HFRAG_VERTS, vertices.size() * sizeof(HfragmentVertex));
tracker->add(MemoryUsageCategory::HFRAG_INDEX, indices.size() * sizeof(u32));
tracker->add(MemoryUsageCategory::HFRAG_TIME_OF_DAY, time_of_day_colors.data.size() * sizeof(u8));
tracker->add(MemoryUsageCategory::HFRAG_CORNERS, corners.size() * sizeof(HfragmentCorner));
}
void Level::memory_usage(MemoryUsageTracker* tracker) const {
for (const auto& texture : textures) {
texture.memory_usage(tracker);
}
for (const auto& texture : index_textures) {
texture.memory_usage(tracker);
}
for (const auto& tftk : tfrag_trees) {
for (const auto& tree : tftk) {
tree.memory_usage(tracker);
}
}
for (const auto& ttk : tie_trees) {
for (const auto& tree : ttk) {
tree.memory_usage(tracker);
}
}
for (const auto& tree : shrub_trees) {
tree.memory_usage(tracker);
}
hfrag.memory_usage(tracker);
collision.memory_usage(tracker);
merc_data.memory_usage(tracker);
}
void print_memory_usage(const tfrag3::Level& lev, int uncompressed_data_size) {
int total_accounted = 0;
MemoryUsageTracker mem_use;
lev.memory_usage(&mem_use);
std::vector<std::pair<std::string, int>> known_categories = {
{"texture", mem_use.data[tfrag3::MemoryUsageCategory::TEXTURE]},
{"special-texture", mem_use.data[tfrag3::MemoryUsageCategory::SPECIAL_TEXTURE]},
{"tie-deinst-vis", mem_use.data[tfrag3::MemoryUsageCategory::TIE_DEINST_VIS]},
{"tie-deinst-idx", mem_use.data[tfrag3::MemoryUsageCategory::TIE_DEINST_INDEX]},
{"tie-inst-vis", mem_use.data[tfrag3::MemoryUsageCategory::TIE_INST_VIS]},
{"tie-inst-idx", mem_use.data[tfrag3::MemoryUsageCategory::TIE_INST_INDEX]},
{"tie-bvh", mem_use.data[tfrag3::MemoryUsageCategory::TIE_BVH]},
{"tie-verts", mem_use.data[tfrag3::MemoryUsageCategory::TIE_VERTS]},
{"tie-colors", mem_use.data[tfrag3::MemoryUsageCategory::TIE_TIME_OF_DAY]},
{"tie-wind-inst-info", mem_use.data[tfrag3::MemoryUsageCategory::TIE_WIND_INSTANCE_INFO]},
{"tie-cidx", mem_use.data[tfrag3::MemoryUsageCategory::TIE_CIDX]},
{"tie-mats", mem_use.data[tfrag3::MemoryUsageCategory::TIE_MATRICES]},
{"tie-grps", mem_use.data[tfrag3::MemoryUsageCategory::TIE_GRPS]},
{"tfrag-vis", mem_use.data[tfrag3::MemoryUsageCategory::TFRAG_VIS]},
{"tfrag-idx", mem_use.data[tfrag3::MemoryUsageCategory::TFRAG_INDEX]},
{"tfrag-vert", mem_use.data[tfrag3::MemoryUsageCategory::TFRAG_VERTS]},
{"tfrag-colors", mem_use.data[tfrag3::MemoryUsageCategory::TFRAG_TIME_OF_DAY]},
{"tfrag-cluster", mem_use.data[tfrag3::MemoryUsageCategory::TFRAG_CLUSTER]},
{"tfrag-bvh", mem_use.data[tfrag3::MemoryUsageCategory::TFRAG_BVH]},
{"shrub-colors", mem_use.data[tfrag3::MemoryUsageCategory::SHRUB_TIME_OF_DAY]},
{"shrub-vert", mem_use.data[tfrag3::MemoryUsageCategory::SHRUB_VERT]},
{"shrub-ind", mem_use.data[tfrag3::MemoryUsageCategory::SHRUB_IND]},
{"shrub-draw", mem_use.data[tfrag3::MemoryUsageCategory::SHRUB_DRAW]},
{"collision", mem_use.data[tfrag3::MemoryUsageCategory::COLLISION]},
{"merc-vert", mem_use.data[tfrag3::MemoryUsageCategory::MERC_VERT]},
{"merc-idx", mem_use.data[tfrag3::MemoryUsageCategory::MERC_INDEX]},
{"merc-draw", mem_use.data[tfrag3::MemoryUsageCategory::MERC_DRAW]},
{"merc-mod-vert", mem_use.data[tfrag3::MemoryUsageCategory::MERC_MOD_VERT]},
{"merc-mod-ind", mem_use.data[tfrag3::MemoryUsageCategory::MERC_MOD_IND]},
{"merc-mod-table", mem_use.data[tfrag3::MemoryUsageCategory::MERC_MOD_TABLE]},
{"merc-mod-draw-1", mem_use.data[tfrag3::MemoryUsageCategory::MERC_MOD_DRAW_1]},
{"merc-mod-draw-2", mem_use.data[tfrag3::MemoryUsageCategory::MERC_MOD_DRAW_2]},
{"blerc", mem_use.data[tfrag3::MemoryUsageCategory::BLERC]},
{"hfrag-verts", mem_use.data[tfrag3::MemoryUsageCategory::HFRAG_VERTS]},
{"hfrag-index", mem_use.data[tfrag3::MemoryUsageCategory::HFRAG_INDEX]},
{"hfrag-time-of-day", mem_use.data[tfrag3::MemoryUsageCategory::HFRAG_TIME_OF_DAY]},
{"hfrag-corners", mem_use.data[tfrag3::MemoryUsageCategory::HFRAG_CORNERS]}
};
for (auto& known : known_categories) {
total_accounted += known.second;
}
known_categories.push_back({"unknown", uncompressed_data_size - total_accounted});
std::sort(known_categories.begin(), known_categories.end(),
[](const auto& a, const auto& b) { return a.second > b.second; });
for (const auto& x : known_categories) {
if (x.second) {
lg::print("{:30s} : {:6d} kB {:3.1f}%\n", x.first, x.second / 1024,
100.f * (float)x.second / uncompressed_data_size);
}
}
}
std::size_t PreloadedVertex::hash::operator()(const PreloadedVertex& v) const {
return std::hash<float>()(v.x) ^ std::hash<float>()(v.y) ^ std::hash<float>()(v.z) ^
std::hash<float>()(v.s) ^ std::hash<float>()(v.t) ^ std::hash<u16>()(v.color_index);
}
} // namespace tfrag3
-628
View File
@@ -1,628 +0,0 @@
#pragma once
// Data format for the tfrag3 renderer.
#include <array>
#include "common/common_types.h"
#include "common/dma/gs.h"
#include "common/math/Vector.h"
#include "common/util/Assert.h"
#include "common/util/Serializer.h"
namespace tfrag3 {
// NOTE:
// when updating any data structures in this file:
// - change the TFRAG3_VERSION
// - make sure to update the serialize function
// - if changing any large things (vertices, vis, bvh, colors, textures) update get_memory_usage
// - if adding a new category to the memory usage, update extract_level to print it.
constexpr int TFRAG3_VERSION = 41;
enum MemoryUsageCategory {
TEXTURE,
SPECIAL_TEXTURE,
TIE_DEINST_VIS,
TIE_DEINST_INDEX,
TIE_INST_VIS,
TIE_INST_INDEX,
TIE_BVH,
TIE_VERTS,
TIE_TIME_OF_DAY,
TIE_WIND_INSTANCE_INFO,
TIE_CIDX,
TIE_MATRICES,
TIE_GRPS,
TFRAG_VIS,
TFRAG_INDEX,
TFRAG_VERTS,
TFRAG_CLUSTER,
TFRAG_TIME_OF_DAY,
TFRAG_BVH,
SHRUB_TIME_OF_DAY,
SHRUB_VERT,
SHRUB_IND,
SHRUB_DRAW,
MERC_VERT,
MERC_INDEX,
MERC_DRAW,
MERC_MOD_DRAW_1,
MERC_MOD_DRAW_2,
MERC_MOD_VERT,
MERC_MOD_IND,
MERC_MOD_TABLE,
BLERC,
HFRAG_VERTS,
HFRAG_INDEX,
HFRAG_TIME_OF_DAY,
HFRAG_CORNERS,
COLLISION,
NUM_CATEGORIES
};
struct MemoryUsageTracker {
u32 data[MemoryUsageCategory::NUM_CATEGORIES];
MemoryUsageTracker() {
for (auto& x : data) {
x = 0;
}
}
void add(MemoryUsageCategory category, u32 size_bytes) { data[category] += size_bytes; }
};
// These vertices should be uploaded to the GPU at load time and don't change
struct PreloadedVertex {
// the vertex position
float x = 0, y = 0, z = 0;
// envmap tint color, not used in == or hash.
u8 r = 0, g = 0, b = 0, a = 0;
// texture coordinates
float s = 0, t = 0;
// not used in == or hash!!
// note that this is a 10-bit 3-element field packed into 32-bits.
u32 nor = 0;
// color table index
u16 color_index = 0;
struct hash {
std::size_t operator()(const PreloadedVertex& x) const;
};
bool operator==(const PreloadedVertex& other) const {
return x == other.x && y == other.y && z == other.z && s == other.s && t == other.t &&
color_index == other.color_index;
}
};
static_assert(sizeof(PreloadedVertex) == 32, "PreloadedVertex size");
struct PackedTieVertices {
struct Vertex {
float x, y, z;
float s, t;
s8 nx, ny, nz;
u8 r, g, b, a;
};
struct MatrixGroup {
s32 matrix_idx;
u32 start_vert;
u32 end_vert;
bool has_normals = false;
};
std::vector<u16> color_indices;
std::vector<std::array<math::Vector4f, 4>> matrices;
std::vector<MatrixGroup> matrix_groups; // todo pack
std::vector<Vertex> vertices;
void serialize(Serializer& ser);
void memory_usage(MemoryUsageTracker* tracker) const;
};
struct PackedTfragVertices {
struct Vertex {
u16 xoff, yoff, zoff;
u16 cluster_idx;
s16 s, t;
u16 color_index;
};
void memory_usage(MemoryUsageTracker* tracker) const;
std::vector<Vertex> vertices;
std::vector<math::Vector<u16, 3>> cluster_origins;
};
struct ShrubGpuVertex {
float x, y, z;
float s, t;
u32 pad0;
u16 color_index;
u16 pad1;
u8 rgba_base[3];
u8 pad2;
};
static_assert(sizeof(ShrubGpuVertex) == 32, "ShrubGpuVertex size");
struct PackedShrubVertices {
struct Vertex {
float x, y, z;
float s, t;
u8 rgba[3];
};
struct InstanceGroup {
s32 matrix_idx;
u32 start_vert;
u32 end_vert;
u16 color_index;
};
std::vector<std::array<math::Vector4f, 4>> matrices;
std::vector<InstanceGroup> instance_groups; // todo pack
std::vector<Vertex> vertices;
u32 total_vertex_count;
void memory_usage(MemoryUsageTracker* tracker) const;
void serialize(Serializer& ser);
};
// Settings for drawing a group of triangle strips.
// This refers to a group of PreloadedVertices that are already uploaded.
// All triangles here are drawn in the same "mode" (blending, texture, etc)
// The vertex index list is chunked by visibility group.
// You can just memcpy the entire list to draw everything, or iterate through visgroups and
// check visibility.
struct StripDraw {
DrawMode mode; // the OpenGL draw settings.
s32 tree_tex_id = 0; // the texture that should be bound for the draw (negative for anim slot)
struct {
u32 idx_of_first_idx_in_full_buffer = 0;
} unpacked;
// indices can be specified as lists of runs and plain indices.
// the runs are still drawn with indexed opengl calls, it just uses less space in the file.
struct VertexRun {
u32 vertex0;
u16 length;
};
std::vector<VertexRun> runs;
std::vector<u32> plain_indices;
// to do culling, the above vertex stream is grouped.
// by following the visgroups and checking the visibility, you can leave out invisible vertices.
struct VisGroup {
u32 num_inds = 0; // number of vertex indices in this group
u32 num_tris = 0; // number of triangles
u16 vis_idx_in_pc_bvh = 0; // the visibility group they belong to (in BVH)
u16 tie_proto_idx = 0; // index of tie proto (tie only)
};
std::vector<VisGroup> vis_groups;
// for debug counting.
u32 num_triangles = 0;
void serialize(Serializer& ser);
};
struct ShrubDraw {
DrawMode mode; // the OpenGL draw settings.
u32 tree_tex_id = 0; // the texture that should be bound for the draw
u32 first_index_index;
u32 num_indices;
// for debug counting.
u32 num_triangles = 0;
u16 proto_idx = 0;
void serialize(Serializer& ser);
};
struct InstancedStripDraw {
DrawMode mode; // the OpenGL draw settings.
u32 tree_tex_id = 0; // the texture that should be bound for the draw
// the list of vertices in the draw. This includes the restart code of UINT32_MAX that OpenGL
// will use to start a new strip.
std::vector<u32> vertex_index_stream;
// the vertex stream above is segmented by instance.
struct InstanceGroup {
u32 num = 0; // number of vertex indices in this group
u32 instance_idx = 0; // the instance they belong to
u32 vis_idx = 0;
};
std::vector<InstanceGroup> instance_groups;
// for debug counting.
u32 num_triangles = 0;
void serialize(Serializer& ser);
void memory_usage(MemoryUsageTracker* tracker) const;
};
// node in the BVH.
struct VisNode {
math::Vector<float, 4> bsphere; // the bounding sphere, in meters (4096 = 1 game meter). w = rad
u16 child_id = 0xffff; // the ID of our first child.
u16 my_id = 0xffff;
u8 num_kids = 0xff; // number of children. The children are consecutive in memory
u8 flags = 0; // flags. If 1, we have a DrawVisNode child, otherwise a leaf.
};
// The leaf nodes don't actually exist in the vector of VisNodes, but instead they are ID's used
// by the actual geometry. Currently we do not include the bspheres of these, but this might be
// worth it if we have a more performant culling algorithm.
struct BVH {
std::vector<VisNode> vis_nodes; // bvh for frustum culling
// additional information about the BVH
u16 first_leaf_node = 0;
u16 last_leaf_node = 0;
u16 first_root = 0;
u16 num_roots = 0;
bool only_children = false;
void serialize(Serializer& ser);
};
// This is split into groups of 4 colors.
// The data in these groups is stored first by palette, then color, then channel.
struct PackedTimeOfDay {
std::vector<u8> data;
u32 color_count = 0;
void serialize(Serializer& ser);
u8 read(int color, int palette, int channel) const {
const int color_quad = color / 4;
const int color_in_quad = color % 4;
return data[color_quad * 4 * 4 * 8 + palette * 4 * 4 + color_in_quad * 4 + channel];
}
u8& read(int color, int palette, int channel) {
const int color_quad = color / 4;
const int color_in_quad = color % 4;
return data[color_quad * 4 * 4 * 8 + palette * 4 * 4 + color_in_quad * 4 + channel];
}
};
// A single texture. Stored as RGBA8888.
struct Texture {
u16 w, h;
u32 combo_id = 0;
std::vector<u32> data;
std::string debug_name;
std::string debug_tpage_name;
bool load_to_pool = false;
void serialize(Serializer& ser);
void memory_usage(MemoryUsageTracker* tracker) const;
};
struct IndexTexture {
u16 w, h;
u32 combo_id = 0;
std::vector<u8> index_data;
std::vector<std::string> level_names;
std::string name;
std::string tpage_name;
std::array<math::Vector4<u8>, 256> color_table;
void serialize(Serializer& ser);
void memory_usage(MemoryUsageTracker* tracker) const;
};
// Tfrag trees have several kinds:
enum class TFragmentTreeKind { NORMAL, TRANS, DIRT, ICE, LOWRES, LOWRES_TRANS, WATER, INVALID };
constexpr const char* tfrag_tree_names[] = {"normal", "trans", "dirt", "ice",
"lowres", "lowres-trans", "water", "invalid"};
// A tfrag model
struct TfragTree {
TFragmentTreeKind kind; // our tfrag kind
std::vector<StripDraw> draws; // the actual topology and settings
PackedTfragVertices packed_vertices;
PackedTimeOfDay colors; // vertex colors (pre-interpolation)
BVH bvh; // the bvh for frustum culling
bool use_strips = true;
struct {
std::vector<PreloadedVertex> vertices; // mesh vertices
std::vector<u32> indices;
} unpacked;
void unpack();
void serialize(Serializer& ser);
void memory_usage(MemoryUsageTracker* tracker) const;
};
struct TieWindInstance {
std::array<math::Vector4f, 4> matrix;
u16 wind_idx;
float stiffness;
void serialize(Serializer& ser);
};
// Tie draws are split into categories.
enum class TieCategory {
// normal tie buckets
NORMAL,
TRANS, // also called alpha
WATER,
// first draw (normal base draw) for envmapped stuff
NORMAL_ENVMAP,
TRANS_ENVMAP,
WATER_ENVMAP,
// second draw (shiny) for envmapped ties.
NORMAL_ENVMAP_SECOND_DRAW,
TRANS_ENVMAP_SECOND_DRAW,
WATER_ENVMAP_SECOND_DRAW,
};
constexpr int kNumTieCategories = 9;
constexpr bool is_envmap_first_draw_category(tfrag3::TieCategory category) {
switch (category) {
case tfrag3::TieCategory::NORMAL_ENVMAP:
case tfrag3::TieCategory::WATER_ENVMAP:
case tfrag3::TieCategory::TRANS_ENVMAP:
return true;
default:
return false;
}
}
constexpr bool is_envmap_second_draw_category(tfrag3::TieCategory category) {
switch (category) {
case tfrag3::TieCategory::NORMAL_ENVMAP_SECOND_DRAW:
case tfrag3::TieCategory::WATER_ENVMAP_SECOND_DRAW:
case tfrag3::TieCategory::TRANS_ENVMAP_SECOND_DRAW:
return true;
default:
return false;
}
}
constexpr TieCategory get_second_draw_category(tfrag3::TieCategory category) {
switch (category) {
case TieCategory::NORMAL_ENVMAP:
return TieCategory::NORMAL_ENVMAP_SECOND_DRAW;
case TieCategory::TRANS_ENVMAP:
return TieCategory::TRANS_ENVMAP_SECOND_DRAW;
case TieCategory::WATER_ENVMAP:
return TieCategory::WATER_ENVMAP_SECOND_DRAW;
default:
return TieCategory::NORMAL_ENVMAP;
}
}
// A tie model
struct TieTree {
BVH bvh;
std::vector<StripDraw> static_draws;
// Category n uses draws: static_draws[cdi[n]] to static_draws[cdi[n + 1]]
std::array<u32, kNumTieCategories + 1> category_draw_indices;
PackedTieVertices packed_vertices;
PackedTimeOfDay colors; // vertex colors (pre-interpolation)
std::vector<InstancedStripDraw> instanced_wind_draws;
std::vector<TieWindInstance> wind_instance_info;
// jak 2 and later can toggle on and off visibility per proto by name
bool has_per_proto_visibility_toggle = false;
std::vector<std::string> proto_names;
struct {
std::vector<PreloadedVertex> vertices; // mesh vertices
std::vector<u32> indices;
} unpacked;
void serialize(Serializer& ser);
void memory_usage(MemoryUsageTracker* tracker) const;
void unpack();
};
struct ShrubTree {
// todo some visibility structure
PackedTimeOfDay time_of_day_colors; // multiplier colors
PackedShrubVertices packed_vertices;
std::vector<ShrubDraw> static_draws; // the actual topology and settings
std::vector<u32> indices;
struct {
std::vector<ShrubGpuVertex> vertices; // mesh vertices
} unpacked;
// jak 2 and later can toggle on and off visibility per proto by name
bool has_per_proto_visibility_toggle = false;
std::vector<std::string> proto_names;
void serialize(Serializer& ser);
void memory_usage(MemoryUsageTracker* tracker) const;
void unpack();
};
struct HfragmentVertex {
float height = 0;
u32 vi = 0;
u16 color_index = 0;
u8 u = 0, v = 0;
u32 pad = 0;
};
struct HfragmentCorner {
math::Vector<float, 4> bsphere;
u32 vis_id = 0;
u32 index_start = 0;
u32 index_length = 0;
u32 num_tris = 0;
};
struct HfragmentBucket {
std::vector<u32> corners;
std::array<u16, 16> montage_table;
void serialize(Serializer& ser);
};
struct Hfragment {
std::vector<HfragmentVertex> vertices;
std::vector<u32> indices;
std::vector<HfragmentCorner> corners;
std::vector<HfragmentBucket> buckets;
PackedTimeOfDay time_of_day_colors;
std::array<s32, 4> wang_tree_tex_id;
DrawMode draw_mode;
u32 occlusion_offset;
void serialize(Serializer& ser);
void memory_usage(MemoryUsageTracker* tracker) const;
};
struct CollisionMesh {
struct Vertex {
float x, y, z;
u32 flags;
s16 nx, ny, nz;
u16 pad;
u32 pat;
u32 pad2;
};
static_assert(sizeof(Vertex) == 32);
std::vector<Vertex> vertices;
void serialize(Serializer& ser);
void memory_usage(MemoryUsageTracker* tracker) const;
};
// MERC
struct MercVertex {
alignas(32) float pos[3];
float pad0;
float normal[3];
float pad1;
float weights[3];
float pad2;
float st[2];
u8 rgba[4];
u8 mats[3];
u8 pad3;
};
static_assert(sizeof(MercVertex) == 64);
struct MercDraw {
DrawMode mode;
s32 tree_tex_id = 0; // the texture that should be bound for the draw (negative for anim slot)
u8 eye_id = 0xff; // 0xff if not eyes, (slot << 1) | (is_r)
u32 first_index;
u32 index_count;
u32 num_triangles;
// no strip hack for custom models
bool no_strip = false;
void serialize(Serializer& ser);
};
struct BlercFloatData {
// [x, y, z, pad, nx, ny, nz, pad]
// note that this should match the layout of the merc vertex above
alignas(32) float v[8];
};
/*!
* Data to modify vertices based on blend shapes.
*/
struct Blerc {
std::vector<BlercFloatData> float_data;
std::vector<u32> int_data;
static constexpr u32 kTargetIdxTerminator = UINT32_MAX;
void serialize(Serializer& ser);
// int data, per vertex:
// [tgt0_idx, tgt1_idx, ..., terminator, dest]
// float data, per vertex:
// [base, tgt0, tgt1, ...]
// final vertex position is:
// base + sum(tgtn * weights[tgtn_idx])
};
struct MercModifiableDrawGroup {
std::vector<MercVertex> vertices;
std::vector<u16> vertex_lump4_addr;
std::vector<MercDraw> fix_draw, mod_draw;
std::vector<u8> fragment_mask;
Blerc blerc;
u32 expect_vidx_end = 0;
void serialize(Serializer& ser);
void memory_usage(MemoryUsageTracker* tracker) const;
};
struct MercEffect {
std::vector<MercDraw> all_draws;
MercModifiableDrawGroup mod;
DrawMode envmap_mode;
u32 envmap_texture;
bool has_envmap = false;
bool has_mod_draw = false;
void serialize(Serializer& ser);
void memory_usage(MemoryUsageTracker* tracker) const;
};
struct MercModel {
std::string name;
std::vector<MercEffect> effects;
u32 max_draws;
u32 max_bones;
u32 st_vif_add;
float xyz_scale;
float st_magic;
void serialize(Serializer& ser);
void memory_usage(MemoryUsageTracker* tracker) const;
};
struct MercModelGroup {
std::vector<MercVertex> vertices;
std::vector<u32> indices;
std::vector<MercModel> models;
void serialize(Serializer& ser);
void memory_usage(MemoryUsageTracker* tracker) const;
};
//
constexpr int TFRAG_GEOS = 3;
constexpr int TIE_GEOS = 4;
struct Level {
u16 version = TFRAG3_VERSION;
std::string level_name;
std::vector<Texture> textures;
std::vector<IndexTexture> index_textures;
std::array<std::vector<TfragTree>, TFRAG_GEOS> tfrag_trees;
std::array<std::vector<TieTree>, TIE_GEOS> tie_trees;
std::vector<ShrubTree> shrub_trees;
Hfragment hfrag;
CollisionMesh collision;
MercModelGroup merc_data;
u16 version2 = TFRAG3_VERSION;
void serialize(Serializer& ser);
void memory_usage(MemoryUsageTracker* tracker) const;
};
void print_memory_usage(const tfrag3::Level& lev, int uncompressed_data_size);
} // namespace tfrag3
-74
View File
@@ -1,74 +0,0 @@
#include "pack_helpers.h"
#include <map>
#include "common/log/log.h"
constexpr float kClusterSize = 4096 * 40; // 100 in-game meters
constexpr float kMasterOffset = 12000 * 4096;
std::pair<u64, u16> position_to_cluster_and_offset(float in) {
in += kMasterOffset;
if (in < 0) {
lg::print("negative: {}\n", in);
}
ASSERT(in >= 0);
int cluster_cell = (in / kClusterSize);
float leftover = in - (cluster_cell * kClusterSize);
u16 offset = (leftover / kClusterSize) * float(UINT16_MAX);
float recovered = ((float)cluster_cell + ((float)offset / UINT16_MAX)) * kClusterSize;
float diff = std::fabs(recovered - in);
ASSERT(diff < 7);
ASSERT(cluster_cell >= 0);
ASSERT(cluster_cell < UINT16_MAX);
return {cluster_cell, offset};
}
void pack_tfrag_vertices(tfrag3::PackedTfragVertices* result,
const std::vector<tfrag3::PreloadedVertex>& vertices) {
u32 next_cluster_idx = 0;
std::map<u64, u32> clusters;
for (auto& vtx : vertices) {
auto x = position_to_cluster_and_offset(vtx.x);
auto y = position_to_cluster_and_offset(vtx.y);
auto z = position_to_cluster_and_offset(vtx.z);
u64 cluster_id = 0;
cluster_id |= x.first;
cluster_id |= (y.first << 16);
cluster_id |= (z.first << 32);
auto cluster_it = clusters.find(cluster_id);
u32 my_cluster_idx = 0;
if (cluster_it == clusters.end()) {
// first in cluster
clusters[cluster_id] = next_cluster_idx;
my_cluster_idx = next_cluster_idx;
next_cluster_idx++;
} else {
my_cluster_idx = cluster_it->second;
}
tfrag3::PackedTfragVertices::Vertex out_vtx;
out_vtx.xoff = x.second;
out_vtx.yoff = y.second;
out_vtx.zoff = z.second;
out_vtx.cluster_idx = my_cluster_idx;
// TODO check these
out_vtx.s = vtx.s * 1024;
out_vtx.t = vtx.t * 1024;
out_vtx.color_index = vtx.color_index;
result->vertices.push_back(out_vtx);
}
result->cluster_origins.resize(next_cluster_idx);
for (auto& cluster : clusters) {
auto& res = result->cluster_origins[cluster.second];
res.x() = (u16)cluster.first;
res.y() = (u16)(cluster.first >> 16);
res.z() = (u16)(cluster.first >> 32);
}
ASSERT(next_cluster_idx < UINT16_MAX);
}
-6
View File
@@ -1,6 +0,0 @@
#pragma once
#include "common/custom_data/Tfrag3Data.h"
void pack_tfrag_vertices(tfrag3::PackedTfragVertices* result,
const std::vector<tfrag3::PreloadedVertex>& vertices);
-129
View File
@@ -1,129 +0,0 @@
#include "dma.h"
#include "common/util/Assert.h"
#include "fmt/core.h"
std::string DmaTag::print() const {
std::string result;
const char* mode_names[8] = {"refe", "cnt", "next", "ref", "refs", "call", "ret", "end"};
result += fmt::format("TAG: 0x{:08x} {:4s} qwc 0x{:04x}", addr, mode_names[(int)kind], qwc);
if (spr) {
result += " SPR";
}
result += "\n";
return result;
}
std::string VifCode::print() const {
std::string result;
switch (kind) {
case Kind::NOP:
result = "NOP";
break;
case Kind::STCYCL: {
VifCodeStcycl stcycl(immediate);
result = fmt::format("STCYCL cl: {} wl: {}", stcycl.cl, stcycl.wl);
} break;
case Kind::OFFSET:
result = fmt::format("OFFSET 0x{:x}", immediate);
break;
case Kind::BASE:
result = fmt::format("BASE 0x{:x}", immediate);
break;
case Kind::ITOP:
result = "ITOP";
break;
case Kind::STMOD:
result = fmt::format("STMOD 0b{:b}", immediate);
break;
case Kind::MSK3PATH:
result = "MSK3PATH";
break;
case Kind::MARK:
result = "MARK";
break;
case Kind::FLUSHE:
result = "FLUSHE";
break;
case Kind::FLUSH:
result = "FLUSH";
break;
case Kind::FLUSHA:
result = "FLUSHA";
break;
case Kind::MSCAL:
result = fmt::format("MSCAL 0x{:x}", immediate);
break;
case Kind::MSCNT:
result = "MSCNT";
break;
case Kind::MSCALF:
result = fmt::format("MSCALF 0x{:x}", immediate);
break;
case Kind::STMASK:
result = "STMASK";
break;
case Kind::STROW:
result = "STROW";
break;
case Kind::STCOL:
result = "STCOL";
break;
case Kind::MPG:
result = "MPG";
break;
case Kind::DIRECT:
result = "DIRECT";
break;
case Kind::DIRECTHL:
result = "DIRECTHL";
break;
case Kind::PC_PORT:
result = "PC_PORT";
break;
case Kind::UNPACK_V4_8: {
VifCodeUnpack up(*this);
result = fmt::format("UNPACK-V4-8: {} addr: {} us: {} tops: {}", num, up.addr_qw,
up.is_unsigned, up.use_tops_flag);
break;
}
case Kind::UNPACK_V4_16: {
VifCodeUnpack up(*this);
result = fmt::format("UNPACK-V4-16: {} addr: {} us: {} tops: {}", num, up.addr_qw,
up.is_unsigned, up.use_tops_flag);
break;
}
case Kind::UNPACK_V4_32: {
VifCodeUnpack up(*this);
result = fmt::format("UNPACK-V4-32: {} addr: {} us: {} tops: {}", num, up.addr_qw,
up.is_unsigned, up.use_tops_flag);
break;
}
case Kind::UNPACK_V3_32: {
VifCodeUnpack up(*this);
result = fmt::format("UNPACK-V3-32: {} addr: {} us: {} tops: {}", num, up.addr_qw,
up.is_unsigned, up.use_tops_flag);
break;
}
case Kind::UNPACK_V2_16: {
VifCodeUnpack up(*this);
result = fmt::format("UNPACK-V2-16: {} addr: {} us: {} tops: {}", num, up.addr_qw,
up.is_unsigned, up.use_tops_flag);
break;
}
default:
result = "???";
ASSERT_MSG(false, fmt::format("Unhandled vif code {}", (int)kind));
break;
}
// TODO: the rest of the VIF code.
return result;
}
-174
View File
@@ -1,174 +0,0 @@
#pragma once
/*!
* @file dma.h
* PS2 DMA and VIF types.
*/
#include <cstring>
#include <string>
#include "common/common_types.h"
#include "common/util/Assert.h"
#include "fmt/core.h"
struct DmaStats {
double sync_time_ms = 0;
int num_tags = 0;
int num_data_bytes = 0;
int num_chunks = 0;
int num_copied_bytes = 0;
int num_fixups = 0;
};
struct DmaTag {
enum class Kind : u8 {
REFE = 0,
CNT = 1,
NEXT = 2,
REF = 3,
REFS = 4,
CALL = 5,
RET = 6,
END = 7
};
DmaTag(u64 value) {
spr = (value >> 63);
addr = (value >> 32) & 0x7fffffff;
qwc = value & 0xfff;
kind = Kind((value >> 28) & 0b111);
}
u16 qwc = 0;
u32 addr = 0;
bool spr = false;
Kind kind;
bool operator==(const DmaTag& other) const {
return qwc == other.qwc && addr == other.addr && spr == other.spr && kind == other.kind;
}
bool operator!=(const DmaTag& other) const { return !((*this) == other); }
std::string print() const;
};
inline void emulate_dma(const void* source_base, void* dest_base, u32 tadr, u32 dadr) {
const u8* src = (const u8*)source_base;
u8* dst = (u8*)dest_base;
u32 dest_offset = dadr;
while (true) {
u64 tag_data;
memcpy(&tag_data, src + tadr, 8);
DmaTag tag(tag_data);
switch (tag.kind) {
case DmaTag::Kind::CNT:
memcpy(dst + dest_offset, src + tadr, (1 + tag.qwc) * 16);
dest_offset += (1 + tag.qwc) * 16;
tadr += 16 + tag.qwc * 16;
break;
case DmaTag::Kind::NEXT:
memcpy(dst + dest_offset, src + tadr, (1 + tag.qwc) * 16);
dest_offset += (1 + tag.qwc) * 16;
tadr = tag.addr;
break;
case DmaTag::Kind::REF: {
// tte
memcpy(dst + dest_offset, src + tadr, 16);
dest_offset += 16;
memcpy(dst + dest_offset, src + tag.addr, tag.qwc * 16);
dest_offset += tag.qwc * 16;
tadr += 16;
} break;
case DmaTag::Kind::REFE: {
// tte
memcpy(dst + dest_offset, src + tadr, 16);
dest_offset += 16;
memcpy(dst + dest_offset, src + tag.addr, tag.qwc * 16);
dest_offset += tag.qwc * 16;
tadr += 16;
return;
} break;
case DmaTag::Kind::END:
// does this transfer anything in TTE???
return;
default:
ASSERT_MSG(false, fmt::format("bad tag: {}", (int)tag.kind));
}
}
}
struct VifCode {
enum class Kind : u8 {
NOP = 0b0,
STCYCL = 0b1,
OFFSET = 0b10,
BASE = 0b11,
ITOP = 0b100,
STMOD = 0b101,
PC_PORT = 0b1000, // not a valid PS2 VIF code, but we use this to signal PC-PORT specific stuff
PC_PORT2 = 0b1001,
MSK3PATH = 0b110,
MARK = 0b111,
FLUSHE = 0b10000,
FLUSH = 0b10001,
FLUSHA = 0b10011,
MSCAL = 0b10100,
MSCNT = 0b10111,
MSCALF = 0b10101,
STMASK = 0b100000,
STROW = 0b110000,
STCOL = 0b110001,
MPG = 0b1001010,
DIRECT = 0b1010000,
DIRECTHL = 0b1010001,
UNPACK_MASK = 0b1100000, // unpack is a bunch of commands.
UNPACK_V4_32 = 0b1101100,
UNPACK_V4_16 = 0b1101101,
UNPACK_V3_32 = 0b1101000,
UNPACK_V4_8 = 0b1101110,
UNPACK_V2_16 = 0b1100101,
};
VifCode(u32 value) {
interrupt = (value) & (1 << 31);
kind = (Kind)((value >> 24) & 0b111'1111);
num = (value >> 16) & 0xff;
immediate = value & 0xffff;
}
bool interrupt = false;
Kind kind;
u16 num;
u16 immediate;
std::string print() const;
};
struct VifCodeStcycl {
explicit VifCodeStcycl(const VifCode& code) {
cl = code.immediate & 0xff;
wl = (code.immediate >> 8);
}
u16 cl;
u16 wl;
};
struct VifCodeUnpack {
explicit VifCodeUnpack(const VifCode& code) {
addr_qw = code.immediate & 0b1111111111;
is_unsigned = (code.immediate & (1 << 14));
use_tops_flag = (code.immediate & (1 << 15));
}
u16 addr_qw;
bool is_unsigned; // only care for 8/16 bit data.
bool use_tops_flag; // uses double buffering
};
-134
View File
@@ -1,134 +0,0 @@
#pragma once
#include <cstring>
#include "common/dma/dma.h"
#include "common/util/Assert.h"
/*!
* @file dma_chain_read.h
* This file contains utilities for reading/following a DMA chain.
*
* This allows you to iterate through transfers like this:
*
* DmaFollower dma(mem, start_addr);
* while (!reader.ended) {
* DmaTransfer transfer = reader.advance_to_next_transfer();
* // do something with the data in transfer.
* }
*/
/*!
* Represents a DMA transfer, including 64-bits of VIF tag.
*/
struct DmaTransfer {
const u8* data = nullptr;
u32 data_offset = 0;
u32 size_bytes = 0;
u64 transferred_tag = 0;
u32 vif0() const { return transferred_tag & 0xffffffff; }
u32 vif1() const { return (transferred_tag >> 32) & 0xffffffff; }
VifCode vifcode0() const { return VifCode(vif0()); }
VifCode vifcode1() const { return VifCode(vif1()); }
template <typename T>
T read_val(u32 offset) const {
T result;
memcpy(&result, (const u8*)data + offset, sizeof(T));
return result;
}
};
class DmaFollower {
public:
DmaFollower() { m_ended = true; }
DmaFollower(const void* data, u32 start_offset) : m_base(data), m_tag_offset(start_offset) {}
template <typename T>
T read_val(u32 offset) const {
T result;
memcpy(&result, (const u8*)m_base + offset, sizeof(T));
return result;
}
/*!
* Read the current tag, return its transfer, then advance to the next.
*/
DmaTransfer read_and_advance() {
DmaTag tag(read_val<u64>(m_tag_offset));
DmaTransfer result;
result.transferred_tag = read_val<u64>(m_tag_offset + 8);
result.size_bytes = (u32)tag.qwc * 16;
ASSERT(!tag.spr);
ASSERT(!m_ended);
switch (tag.kind) {
case DmaTag::Kind::CNT:
// data, then next tag. doesn't read address.
ASSERT(tag.addr == 0);
result.data_offset = m_tag_offset + 16;
m_tag_offset = result.data_offset + result.size_bytes;
break;
case DmaTag::Kind::NEXT:
result.data_offset = m_tag_offset + 16;
m_tag_offset = tag.addr;
break;
case DmaTag::Kind::REF:
case DmaTag::Kind::REFS:
result.data_offset = tag.addr;
m_tag_offset = m_tag_offset + 16;
break;
case DmaTag::Kind::REFE:
result.data_offset = tag.addr;
m_tag_offset = m_tag_offset + 16;
m_ended = true;
break;
case DmaTag::Kind::CALL:
result.data_offset = m_tag_offset + 16;
ASSERT(m_sp <= 1);
m_tag_offset = tag.addr;
m_stack[m_sp++] = result.data_offset + tag.qwc * 16;
break;
case DmaTag::Kind::RET:
ASSERT(m_sp > 0);
result.data_offset = m_tag_offset + 16;
m_tag_offset = m_stack[--m_sp];
break;
case DmaTag::Kind::END:
result.data_offset = m_tag_offset + 16;
m_ended = true;
break;
default:
ASSERT(false);
}
result.data = (const u8*)m_base + result.data_offset;
return result;
}
DmaTransfer advance_and_print_dma(DmaFollower& dma) {
auto data = dma.read_and_advance();
printf(
"dma transfer:\n%ssize: %d\nvif0: %s, data: %d\nvif1: %s, data: %d, imm: "
"%d\n\n",
dma.current_tag().print().c_str(), data.size_bytes, data.vifcode0().print().c_str(),
data.vif0(), data.vifcode1().print().c_str(), data.vifcode1().num,
data.vifcode1().immediate);
return data;
}
DmaTag current_tag() const { return DmaTag(read_val<u64>(m_tag_offset)); }
u32 current_tag_vif0() const { return read_val<u32>(m_tag_offset + 8); }
u32 current_tag_vif1() const { return read_val<u32>(m_tag_offset + 12); }
VifCode current_tag_vifcode0() const { return VifCode(current_tag_vif0()); }
VifCode current_tag_vifcode1() const { return VifCode(current_tag_vif1()); }
u32 current_tag_offset() const { return m_tag_offset; }
bool ended() const { return m_ended; }
private:
const void* m_base = nullptr;
u32 m_tag_offset = 0;
s32 m_sp = 0;
s32 m_stack[2] = {-1, -1};
bool m_ended = false;
};
-198
View File
@@ -1,198 +0,0 @@
#include "dma_copy.h"
#include "common/dma/dma_chain_read.h"
#include "common/goal_constants.h"
#include "common/log/log.h"
#include "common/util/Timer.h"
#include "fmt/core.h"
/*!
* Convert a DMA chain to an array of bytes that can be directly fed to VIF.
*/
std::vector<u8> flatten_dma(const DmaFollower& in) {
DmaFollower state = in;
std::vector<u8> result;
while (!state.ended()) {
auto read_result = state.read_and_advance();
// insert 1 quadword of zeros.
// the first 8 bytes will remain zero and will be interpreted as VIF nop.
for (int i = 0; i < 16; i++) {
result.push_back(0);
}
// the second will contain the transferred tag.
memcpy(result.data() + result.size() - 8, &read_result.transferred_tag, 8);
// and the actual DMA data.
result.insert(result.end(), read_result.data, read_result.data + read_result.size_bytes);
}
return result;
}
void diff_dma_chains(DmaFollower ref, DmaFollower dma) {
while (!ref.ended() && !dma.ended()) {
auto ref_tag = ref.current_tag();
auto dma_tag = dma.current_tag();
if (ref_tag.kind != dma_tag.kind) {
lg::warn("Bad dma tag kinds");
}
if (ref_tag.qwc != dma_tag.qwc) {
lg::warn("Bad dma tag qwc: {} {}", ref_tag.qwc, dma_tag.qwc);
}
auto ref_result = ref.read_and_advance();
auto dma_result = dma.read_and_advance();
for (int i = 0; i < (int)ref_result.size_bytes; i++) {
if (ref_result.data[i] != dma_result.data[i]) {
lg::error("Bad data ({} vs {}) at {} into transfer: {} {}", ref_result.data[i],
dma_result.data[i], i, ref_tag.print(), dma_tag.print());
return;
}
}
}
if (!ref.ended()) {
lg::warn("dma ended early");
}
if (!dma.ended()) {
lg::warn("dma had extra data");
}
}
void FixedChunkDmaCopier::serialize_last_result(Serializer& serializer) {
serializer.from_ptr(&m_result.start_offset);
serializer.from_pod_vector(&m_result.data);
}
FixedChunkDmaCopier::FixedChunkDmaCopier(u32 main_memory_size)
: m_main_memory_size(main_memory_size), m_chunk_count(main_memory_size / chunk_size) {
ASSERT(chunk_size * m_chunk_count == m_main_memory_size); // make sure the memory size is valid.
ASSERT(chunk_size >= 16);
m_chunk_mask.resize(m_chunk_count);
}
void FixedChunkDmaCopier::set_input_data(const void* memory, u32 offset, bool run_copy) {
if (run_copy) {
run(memory, offset, false);
} else {
m_input_offset = offset;
m_input_data = memory;
}
}
const DmaData& FixedChunkDmaCopier::run(const void* memory, u32 offset, bool verify) {
Timer timer;
m_input_offset = offset;
m_input_data = memory;
std::fill(m_chunk_mask.begin(), m_chunk_mask.end(), false);
m_fixups.clear();
m_result.data.clear();
m_result.stats = DmaStats();
m_result.start_offset = 0;
DmaFollower dma(memory, offset);
while (!dma.ended()) {
auto tag_offset = dma.current_tag_offset();
auto tag = dma.current_tag();
m_result.stats.num_tags++;
// first, make sure we get this tag:
u32 tag_chunk_idx = tag_offset / chunk_size;
u32 tag_offset_in_chunk = tag_offset % chunk_size;
m_chunk_mask.at(tag_chunk_idx) = true;
if (tag.addr) {
ASSERT(tag.addr > EE_MAIN_MEM_LOW_PROTECT);
u32 addr_chunk_idx = tag.addr / chunk_size;
u32 addr_offset_in_chunk = tag.addr % chunk_size;
// next, make sure that we get the address (if applicable)
m_chunk_mask.at(addr_chunk_idx) = true;
// and add a fixup
Fixup fu;
fu.source_chunk = tag_chunk_idx;
fu.offset_in_source_chunk = tag_offset_in_chunk;
fu.dest_chunk = addr_chunk_idx;
fu.offset_in_dest_chunk = addr_offset_in_chunk;
m_fixups.push_back(fu);
}
auto transfer = dma.read_and_advance();
if (transfer.size_bytes) {
m_result.stats.num_data_bytes += transfer.size_bytes;
u32 initial_chunk = transfer.data_offset / chunk_size;
u32 end_addr = transfer.data_offset + transfer.size_bytes;
m_chunk_mask.at(initial_chunk) = true;
u32 chunk = initial_chunk + 1;
u32 current_address = chunk_size * chunk;
while (current_address < end_addr) {
current_address += chunk_size;
m_chunk_mask.at(chunk++) = true;
}
}
}
// assign output chunks.
u32 current_out_chunk = 0;
for (auto& val : m_chunk_mask) {
if (val) {
val = current_out_chunk++;
} else {
val = UINT32_MAX;
}
}
m_result.data.resize(current_out_chunk * chunk_size);
m_result.stats.num_chunks = current_out_chunk;
m_result.stats.num_copied_bytes = m_result.data.size();
m_result.stats.num_fixups = m_fixups.size();
// copy
for (u32 chunk_idx = 0; chunk_idx < m_chunk_mask.size(); chunk_idx++) {
u32 dest_idx = m_chunk_mask[chunk_idx];
if (dest_idx != UINT32_MAX) {
memcpy(m_result.data.data() + (dest_idx * chunk_size),
(const u8*)memory + (chunk_idx * chunk_size), chunk_size);
}
}
// fix up!
for (const auto& fu : m_fixups) {
u32 tag_addr = m_chunk_mask.at(fu.source_chunk) * chunk_size + fu.offset_in_source_chunk + 4;
u32 dest_addr = m_chunk_mask.at(fu.dest_chunk) * chunk_size + fu.offset_in_dest_chunk;
memcpy(m_result.data.data() + tag_addr, &dest_addr, 4);
}
// setup final offset
m_result.start_offset = m_chunk_mask.at(offset / chunk_size) * chunk_size + (offset % chunk_size);
if (verify) {
auto ref = flatten_dma(DmaFollower(memory, offset));
auto v2 = flatten_dma(DmaFollower(m_result.data.data(), m_result.start_offset));
if (ref != v2) {
lg::error("Verification has failed.");
lg::error("size diff: {} {}", ref.size(), v2.size());
for (size_t i = 0; i < std::min(ref.size(), v2.size()); i++) {
if (ref[i] != v2[i]) {
lg::error("first diff at {}", i);
break;
}
}
diff_dma_chains(DmaFollower(memory, offset),
DmaFollower(m_result.data.data(), m_result.start_offset));
ASSERT(false);
} else {
lg::debug("verification ok: {} bytes", ref.size());
}
}
m_result.stats.sync_time_ms = timer.getMs();
return m_result;
}
-57
View File
@@ -1,57 +0,0 @@
#pragma once
#include <vector>
#include "common/common_types.h"
#include "common/dma/dma_chain_read.h"
#include "common/util/Serializer.h"
struct DmaData {
u32 start_offset = 0;
std::vector<u8> data;
DmaStats stats;
};
/*!
* The fixed chunk copier considers the game's main memory as an array of fixed sized chunks.
* Only the chunks that have dma data are included in the copy.
* The result is cached internally.
*/
class FixedChunkDmaCopier {
public:
static constexpr u32 chunk_size = 0x20000; // 128 kB, gives use 1024 chunks for a 128 MB RAM.
FixedChunkDmaCopier(u32 main_memory_size);
void set_input_data(const void* memory, u32 offset, bool run);
const DmaData& run(const void* memory, u32 offset, bool verify = false);
void serialize_last_result(Serializer& serializer);
const DmaData& get_last_result() const { return m_result; }
const void* get_last_input_data() const { return m_input_data; }
u32 get_last_input_offset() const { return m_input_offset; }
private:
struct Fixup {
u32 source_chunk;
u32 offset_in_source_chunk;
u32 dest_chunk;
u32 offset_in_dest_chunk;
};
std::vector<Fixup> m_fixups;
u32 m_main_memory_size = 0;
u32 m_chunk_count = 0;
std::vector<u32> m_chunk_mask;
DmaData m_result;
u32 m_input_offset = 0;
const void* m_input_data = nullptr;
};
/*!
* Convert a DMA chain to an array of bytes that can be directly fed to VIF.
*/
std::vector<u8> flatten_dma(const DmaFollower& in);
-464
View File
@@ -1,464 +0,0 @@
#include "gs.h"
#include "common/util/Assert.h"
#include "fmt/core.h"
#include "fmt/format.h"
std::string reg_descriptor_name(GifTag::RegisterDescriptor reg) {
switch (reg) {
case GifTag::RegisterDescriptor::PRIM:
return "PRIM";
case GifTag::RegisterDescriptor::RGBAQ:
return "RGBAQ";
case GifTag::RegisterDescriptor::ST:
return "ST";
case GifTag::RegisterDescriptor::UV:
return "UV";
case GifTag::RegisterDescriptor::XYZF2:
return "XYZF2";
case GifTag::RegisterDescriptor::XYZ2:
return "XYZ2";
case GifTag::RegisterDescriptor::TEX0_1:
return "TEX0_1";
case GifTag::RegisterDescriptor::TEX0_2:
return "TEX0_2";
case GifTag::RegisterDescriptor::CLAMP_1:
return "CLAMP_1";
case GifTag::RegisterDescriptor::CLAMP_2:
return "CLAMP_2";
case GifTag::RegisterDescriptor::FOG:
return "FOG";
case GifTag::RegisterDescriptor::XYZF3:
return "XYZF3";
case GifTag::RegisterDescriptor::XYZ3:
return "XYZ3";
case GifTag::RegisterDescriptor::AD:
return "A+D";
case GifTag::RegisterDescriptor::NOP:
return "NOP";
default:
ASSERT(false);
}
}
std::string GifTag::print() const {
std::string result;
switch (flg()) {
case Format::PACKED:
result += "packed ";
break;
case Format::REGLIST:
result += "reglist ";
break;
case Format::IMAGE:
result += "image ";
return result;
case Format::DISABLE:
result += "disable ";
return result;
default:
ASSERT(false);
}
result += fmt::format("nloop: {} ", nloop());
if (pre()) {
result += fmt::format("prim: 0x{:x} ", prim());
}
if (eop()) {
result += "eop ";
}
result += '\n';
result += ' ';
for (u32 i = 0; i < nreg(); i++) {
result += reg_descriptor_name(reg(i));
result += ' ';
}
result += "\n";
return result;
}
std::string register_address_name(u32 reg) {
return register_address_name(GsRegisterAddress(reg));
}
std::string register_address_name(GsRegisterAddress reg) {
switch (reg) {
case GsRegisterAddress::PRIM:
return "PRIM";
case GsRegisterAddress::RGBAQ:
return "RGBAQ";
case GsRegisterAddress::ST:
return "ST";
case GsRegisterAddress::UV:
return "UV";
case GsRegisterAddress::XYZF2:
return "XYZF2";
case GsRegisterAddress::XYZ2:
return "XYZ2";
case GsRegisterAddress::TEX0_1:
return "TEX0_1";
case GsRegisterAddress::TEX0_2:
return "TEX0_2";
case GsRegisterAddress::CLAMP_1:
return "CLAMP_1";
case GsRegisterAddress::CLAMP_2:
return "CLAMP_2";
case GsRegisterAddress::FOG:
return "FOG";
case GsRegisterAddress::XYZF3:
return "XYZF3";
case GsRegisterAddress::XYZ3:
return "XYZ3";
case GsRegisterAddress::TEX1_1:
return "TEX1_1";
case GsRegisterAddress::TEX1_2:
return "TEX1_2";
case GsRegisterAddress::TEX2_1:
return "TEX2_1";
case GsRegisterAddress::TEX2_2:
return "TEX2_2";
case GsRegisterAddress::XYOFFSET_1:
return "XYOFFSET_1";
case GsRegisterAddress::XYOFFSET_2:
return "XYOFFSET_2";
case GsRegisterAddress::PRMODECONT:
return "PRMODECONT";
case GsRegisterAddress::PRMODE:
return "PRMODE";
case GsRegisterAddress::TEXCLUT:
return "TEXCLUT";
case GsRegisterAddress::SCANMSK:
return "SCANMSK";
case GsRegisterAddress::MIPTBP1_1:
return "MIPTBP1_1";
case GsRegisterAddress::MIPTBP1_2:
return "MIPTBP1_2";
case GsRegisterAddress::MIPTBP2_1:
return "MIPTBP2_1";
case GsRegisterAddress::MIPTBP2_2:
return "MIPTBP2_2";
case GsRegisterAddress::TEXA:
return "TEXA";
case GsRegisterAddress::FOGCOL:
return "FOGCOL";
case GsRegisterAddress::TEXFLUSH:
return "TEXFLUSH";
case GsRegisterAddress::SCISSOR_1:
return "SCISSOR_1";
case GsRegisterAddress::SCISSOR_2:
return "SCISSOR_2";
case GsRegisterAddress::ALPHA_1:
return "ALPHA_1";
case GsRegisterAddress::ALPHA_2:
return "ALPHA_2";
case GsRegisterAddress::DIMX:
return "DIMX";
case GsRegisterAddress::DTHE:
return "DTHE";
case GsRegisterAddress::COLCLAMP:
return "COLCLAMP";
case GsRegisterAddress::TEST_1:
return "TEST_1";
case GsRegisterAddress::TEST_2:
return "TEST_2";
case GsRegisterAddress::PABE:
return "PABE";
case GsRegisterAddress::FBA_1:
return "FBA_1";
case GsRegisterAddress::FBA_2:
return "FBA_2";
case GsRegisterAddress::FRAME_1:
return "FRAME_1";
case GsRegisterAddress::FRAME_2:
return "FRAME_2";
case GsRegisterAddress::ZBUF_1:
return "ZBUF_1";
case GsRegisterAddress::ZBUF_2:
return "ZBUF_2";
case GsRegisterAddress::BITBLTBUF:
return "BITBLTBUF";
case GsRegisterAddress::TRXPOS:
return "TRXPOS";
case GsRegisterAddress::TRXREG:
return "TRXREG";
case GsRegisterAddress::TRXDIR:
return "TRXDIR";
case GsRegisterAddress::HWREG:
return "HWREG";
case GsRegisterAddress::SIGNAL:
return "SIGNAL";
case GsRegisterAddress::FINISH:
return "FINISH";
case GsRegisterAddress::LABEL:
return "LABEL";
default:
ASSERT(false);
}
}
std::string GsTest::print() const {
std::string result;
if (alpha_test_enable()) {
result += "alpha-test: ";
switch (alpha_test()) {
case AlphaTest::NEVER:
result += "NEVER ";
break;
case AlphaTest::ALWAYS:
result += "ALWAYS ";
break;
case AlphaTest::LESS:
result += "LESS ";
break;
case AlphaTest::LEQUAL:
result += "LEQUAL ";
break;
case AlphaTest::EQUAL:
result += "EQUAL ";
break;
case AlphaTest::GEQUAL:
result += "GEQUAL ";
break;
case AlphaTest::GREATER:
result += "GREATER ";
break;
case AlphaTest::NOTEQUAL:
result += "NOTEQUAL ";
break;
default:
ASSERT(false);
}
result += fmt::format("ref: 0x{:x} alpha-fail: ", aref());
switch (afail()) {
case AlphaFail::KEEP:
result += "KEEP ";
break;
case AlphaFail::FB_ONLY:
result += "FB_ONLY ";
break;
case AlphaFail::ZB_ONLY:
result += "ZB_ONLY ";
break;
case AlphaFail::RGB_ONLY:
result += "RGB_ONLY ";
break;
default:
ASSERT(false);
}
}
if (date()) {
result += fmt::format("dest-alpha-mode {} ", (int)datm());
}
if (zte()) {
result += fmt::format("ztest: ");
switch (ztest()) {
case ZTest::NEVER:
result += "NEVER";
break;
case ZTest::ALWAYS:
result += "ALWAYS";
break;
case ZTest::GEQUAL:
result += "GEQUAL";
break;
case ZTest::GREATER:
result += "GREATER";
break;
default:
ASSERT(false);
}
}
result += '\n';
return result;
}
std::string GsAlpha::print() const {
std::string result = "(";
switch (a_mode()) {
case BlendMode::SOURCE:
result += "Cs ";
break;
case BlendMode::DEST:
result += "Cd ";
break;
case BlendMode::ZERO_OR_FIXED:
result += "0 ";
break;
default:
ASSERT(false);
}
switch (b_mode()) {
case BlendMode::SOURCE:
result += "- Cs) * ";
break;
case BlendMode::DEST:
result += "- Cd) * ";
break;
case BlendMode::ZERO_OR_FIXED:
result += "- 0) * ";
break;
default:
ASSERT(false);
}
switch (c_mode()) {
case BlendMode::SOURCE:
result += "As / 128.0";
break;
case BlendMode::DEST:
result += "Ad / 128.0";
break;
case BlendMode::ZERO_OR_FIXED: {
float div = fix();
div /= 128.0;
result += fmt::format("{:.4f}", div);
} break;
default:
ASSERT(false);
}
switch (d_mode()) {
case BlendMode::SOURCE:
result += " + Cs";
break;
case BlendMode::DEST:
result += " + Cd";
break;
case BlendMode::ZERO_OR_FIXED:
break;
default:
ASSERT(false);
}
return result;
}
std::string GsTex1::print() const {
return fmt::format("lcm: {} mxl: {} mmag: {} mmin: {} mtba: {} l: {} k: {}\n", lcm(), mxl(),
mmag(), mmin(), mtba(), l(), k());
}
std::string GsTexa::print() const {
return fmt::format("ta0: {} aem: {} ta1: {}\n", ta0(), aem(), ta1());
}
std::string GsTex0::print() const {
return fmt::format(
"tbp0: {} tbw: {} psm: {} tw: {} th: {} tcc: {} tfx: {} cbp: {} cpsm: {} csm: {}\n", tbp0(),
tbw(), fmt::underlying(psm()), tw(), th(), tcc(), fmt::underlying(tfx()), cbp(), cpsm(),
csm());
}
std::string GsPrim::print() const {
return fmt::format("0x{:x}, kind {}\n", data, fmt::underlying(kind()));
}
std::string GsFrame::print() const {
return fmt::format("fbp: {} fbw: {} psm: {} fbmsk: {:x}\n", fbp(), fbw(), fmt::underlying(psm()),
fbmsk());
}
std::string GsXYOffset::print() const {
return fmt::format("ofx: {} ofy: {}\n", ofx(), ofy());
}
std::string DrawMode::to_string() const {
std::string result;
result += fmt::format(" depth-write: {}\n", get_depth_write_enable());
result += fmt::format(" depth-test: ");
switch (get_depth_test()) {
case GsTest::ZTest::NEVER:
result += "never\n";
break;
case GsTest::ZTest::GEQUAL:
result += "gequal\n";
break;
case GsTest::ZTest::ALWAYS:
result += "always\n";
break;
case GsTest::ZTest::GREATER:
result += "greater\n";
break;
default:
ASSERT(false);
}
result += fmt::format(" alpha: ");
switch (get_alpha_blend()) {
case AlphaBlend::SRC_0_SRC_DST:
result += "src, 0, src, dst\n";
break;
case AlphaBlend::SRC_DST_SRC_DST:
result += "src, dst, src, dst\n";
break;
case AlphaBlend::DISABLED:
result += "disabled\n";
break;
case AlphaBlend::SRC_DST_FIX_DST:
result += "src, dst, fix, dst\n";
break;
case AlphaBlend::SRC_0_DST_DST:
result += "src, 0, dst, dst\n";
break;
case AlphaBlend::SRC_SRC_SRC_SRC:
result += "src, src, src, src\n";
break;
case AlphaBlend::ZERO_SRC_SRC_DST:
result += "0, src, src, dst\n";
break;
case AlphaBlend::SRC_0_FIX_DST:
result += "src, 0, fix, dst\n";
break;
default:
ASSERT(false);
}
result += fmt::format(" clamp: s {} t {}\n", get_clamp_s_enable(), get_clamp_t_enable());
result += fmt::format(" filt: {}\n", get_filt_enable());
result += fmt::format(" tcc: {}\n", get_tcc_enable());
result += fmt::format(" aref: {}\n", get_aref());
result += fmt::format(" ate: {}\n", get_at_enable());
result += fmt::format(" atst: ");
switch (get_alpha_test()) {
case AlphaTest::ALWAYS:
result += "always\n";
break;
case AlphaTest::GEQUAL:
result += "gequal\n";
break;
case AlphaTest::NEVER:
result += "never\n";
break;
default:
result += "invalid!\n";
break;
}
result += fmt::format(" zte: {}\n", get_zt_enable());
result += fmt::format(" abe: {}\n", get_ab_enable());
result += fmt::format(" afail: ");
switch (get_alpha_fail()) {
case GsTest::AlphaFail::KEEP:
result += "keep\n";
break;
case GsTest::AlphaFail::FB_ONLY:
result += "fb-only\n";
break;
case GsTest::AlphaFail::RGB_ONLY:
result += "rgb-only\n";
break;
case GsTest::AlphaFail::ZB_ONLY:
result += "zb-only\n";
break;
default:
ASSERT(false);
}
result += fmt::format(" fog: {}\n decal: {}\n", get_fog_enable(), get_decal());
return result;
}
-592
View File
@@ -1,592 +0,0 @@
#pragma once
#include "common/dma/dma.h"
/*!
* @file gs.h
* PS2 GS/GIF hardware types
*/
struct GifTag {
enum class Format : u8 { PACKED = 0, REGLIST = 1, IMAGE = 2, DISABLE = 3 };
enum class RegisterDescriptor : u8 {
PRIM = 0,
RGBAQ = 1,
ST = 2,
UV = 3,
XYZF2 = 4,
XYZ2 = 5,
TEX0_1 = 6,
TEX0_2 = 7,
CLAMP_1 = 8,
CLAMP_2 = 9,
FOG = 10,
// no 11
XYZF3 = 12,
XYZ3 = 13,
AD = 14,
NOP = 15,
};
u32 nloop() const {
return data[0] & 0x7fff; // 15 bits
}
bool eop() const { return data[0] & (1ull << 15); }
bool pre() const { return data[0] & (1ull << 46); }
u32 prim() const { return (data[0] >> 47) & 0b111'1111'1111; }
Format flg() const { return (Format)((data[0] >> 58) & 0b11); }
u32 nreg() const {
u32 result = (data[0] >> 60) & 0b1111;
if (!result) {
return 16;
} else {
return result;
}
}
RegisterDescriptor reg(u32 idx) const {
return (RegisterDescriptor)((data[1] >> (4 * idx)) & 0b1111);
}
std::string print() const;
GifTag(const u8* ptr) { memcpy(data, ptr, 16); }
GifTag() = default;
u64 data[2];
};
std::string reg_descriptor_name(GifTag::RegisterDescriptor reg);
enum class GsRegisterAddress : u8 {
PRIM = 0,
RGBAQ = 1,
ST = 2,
UV = 3,
XYZF2 = 4,
XYZ2 = 5,
TEX0_1 = 6,
TEX0_2 = 7,
CLAMP_1 = 8,
CLAMP_2 = 9,
FOG = 0xa,
XYZF3 = 0xc,
XYZ3 = 0xd,
TEX1_1 = 0x14,
TEX1_2 = 0x15,
TEX2_1 = 0x16,
TEX2_2 = 0x17,
XYOFFSET_1 = 0x18,
XYOFFSET_2 = 0x19,
PRMODECONT = 0x1a,
PRMODE = 0x1b,
TEXCLUT = 0x1c,
SCANMSK = 0x22,
MIPTBP1_1 = 0x34,
MIPTBP1_2 = 0x35,
MIPTBP2_1 = 0x36,
MIPTBP2_2 = 0x37,
TEXA = 0x3b,
FOGCOL = 0x3d,
TEXFLUSH = 0x3f,
SCISSOR_1 = 0x40,
SCISSOR_2 = 0x41,
ALPHA_1 = 0x42,
ALPHA_2 = 0x43,
DIMX = 0x44,
DTHE = 0x45,
COLCLAMP = 0x46,
TEST_1 = 0x47,
TEST_2 = 0x48,
PABE = 0x49,
FBA_1 = 0x4a,
FBA_2 = 0x4b,
FRAME_1 = 0x4c,
FRAME_2 = 0x4d,
ZBUF_1 = 0x4e,
ZBUF_2 = 0x4f,
BITBLTBUF = 0x50,
TRXPOS = 0x51,
TRXREG = 0x52,
TRXDIR = 0x53,
HWREG = 0x54,
SIGNAL = 0x60,
FINISH = 0x61,
LABEL = 0x62
};
enum class TextureFormat { PSMZ32, PSMZ24, PSMZ16, PSMZ16S };
std::string register_address_name(GsRegisterAddress reg);
std::string register_address_name(u32 reg);
struct GsZbuf {
GsZbuf(u64 val) : data(val) {}
GsZbuf() = default;
u32 zbp() const { return data & 0b1'1111'1111; }
TextureFormat psm() const {
u32 psm_field = (data >> 24) & 0b1111;
switch (psm_field) {
case 0b0000:
return TextureFormat::PSMZ32;
case 0b0001:
return TextureFormat::PSMZ24;
case 0b0010:
return TextureFormat::PSMZ16;
case 0b1010:
return TextureFormat::PSMZ16S;
default:
ASSERT(false);
}
}
bool zmsk() const { return data & (1ull << 32); }
u64 data;
};
struct GsScissor {
GsScissor(u64 val) : data(val) {}
u32 x0() const { return data & 0b11111111111; }
u32 x1() const { return (data >> 16) & 0b11111111111; }
u32 y0() const { return (data >> 32) & 0b11111111111; }
u32 y1() const { return (data >> 48) & 0b11111111111; }
u64 data;
};
struct GsTest {
GsTest() = default;
GsTest(u64 val) : data(val) {}
bool alpha_test_enable() const { return data & 1; }
enum class AlphaTest : u8 {
NEVER = 0,
ALWAYS = 1,
LESS = 2,
LEQUAL = 3,
EQUAL = 4,
GEQUAL = 5,
GREATER = 6,
NOTEQUAL = 7
};
AlphaTest alpha_test() const { return (AlphaTest)((data >> 1) & 0b111); }
u8 aref() const { return (data >> 4); }
enum class AlphaFail : u8 { KEEP = 0, FB_ONLY = 1, ZB_ONLY = 2, RGB_ONLY = 3 };
AlphaFail afail() const { return (AlphaFail)((data >> 12) & 0b11); }
bool date() const { return data & (1 << 14); }
bool datm() const { return data & (1 << 15); }
bool zte() const { return data & (1 << 16); }
enum class ZTest : u8 { NEVER = 0, ALWAYS = 1, GEQUAL = 2, GREATER = 3 };
ZTest ztest() const { return (ZTest)((data >> 17) & 0b11); }
std::string print() const;
u64 data = 0;
bool operator==(const GsTest& other) const { return data == other.data; }
bool operator!=(const GsTest& other) const { return data != other.data; }
};
struct GsAlpha {
GsAlpha() = default;
GsAlpha(u64 val) : data(val) {}
enum class BlendMode {
SOURCE = 0,
DEST = 1, // frame buffer
ZERO_OR_FIXED = 2, // 0 for a, b, d, fixed for c
INVALID = 3
};
BlendMode a_mode() const { return (BlendMode)(data & 0b11); }
BlendMode b_mode() const { return (BlendMode)((data >> 2) & 0b11); }
BlendMode c_mode() const { return (BlendMode)((data >> 4) & 0b11); }
BlendMode d_mode() const { return (BlendMode)((data >> 6) & 0b11); }
u8 fix() const { return (data >> 32); }
std::string print() const;
u64 data = 0;
bool operator==(const GsAlpha& other) const { return data == other.data; }
bool operator!=(const GsAlpha& other) const { return data != other.data; }
};
struct GsPrim {
GsPrim() = default;
GsPrim(u64 val) : data(val & 0b111'1111'1111) {}
enum class Kind {
POINT = 0,
LINE = 1,
LINE_STRIP = 2,
TRI = 3,
TRI_STRIP = 4,
TRI_FAN = 5,
SPRITE = 6,
PRIM_7 = 7
};
Kind kind() const { return (Kind)(data & 0b111); }
bool gouraud() const { // iip
return data & (1 << 3);
}
bool tme() const { return data & (1 << 4); }
bool fge() const { return data & (1 << 5); }
bool abe() const { return data & (1 << 6); }
bool aa1() const { return data & (1 << 7); }
bool fst() const { return data & (1 << 8); }
bool ctxt() const { return data & (1 << 9); }
bool fix() const { return data & (1 << 10); }
u64 data = 0;
bool operator==(const GsPrim& other) const { return data == other.data; }
bool operator!=(const GsPrim& other) const { return data != other.data; }
std::string print() const;
};
struct GsTex0 {
GsTex0() = default;
GsTex0(u64 val) : data(val) {}
u32 tbp0() const { return data & 0b11'1111'1111'1111; }
u32 tbw() const { return (data >> 14) & 0b111111; }
enum class PSM {
PSMCT32 = 0,
PSMCT24 = 1,
PSMCT16 = 2,
PSMCT16S = 0b1010,
PSMT8 = 0b10011,
PSMT4 = 0b10100,
PSMT8H = 0b011011,
PSMT4HL = 0b100100,
PSMT4HH = 0b101100,
PSMZ32 = 0b110000,
PSMZ24 = 0b110001,
PSMZ16 = 0b110010,
PSMZ16S = 0b111010
};
PSM psm() const { return (PSM)((data >> 20) & 0b111111); }
u32 tw() const { return (data >> 26) & 0b1111; }
u32 th() const { return (data >> 30) & 0b1111; }
enum class TextureFunction : u8 {
MODULATE = 0,
DECAL = 1,
HIGHLIGHT = 2,
HIGHLIGHT2 = 3,
};
u32 tcc() const { return ((data >> 34) & 0b1); }
TextureFunction tfx() const { return (TextureFunction)((data >> 35) & 0b11); }
u32 cbp() const { return (data >> 37) & 0b11'1111'1111'1111; }
u32 cpsm() const { return (data >> 51) & 0b1111; }
u32 csm() const { return (data >> 55) & 1; }
std::string print() const;
bool operator==(const GsTex0& other) const { return data == other.data; }
bool operator!=(const GsTex0& other) const { return data != other.data; }
u64 data = 0;
};
struct GsTex1 { // tex1_1 and tex1_2
GsTex1() = default;
GsTex1(u64 val) : data(val) {}
bool lcm() const {
// 1 = fixed
return data & 1;
}
u32 mxl() const { return (data >> 2) & 0b111; }
bool mmag() const { return data & (1 << 5); }
u32 mmin() const { return (data >> 6) & 0b111; }
bool mtba() const { return data & (1 << 9); }
u32 l() const { return (data >> 19) & 0b11; }
u32 k() const { return (data >> 32) & 0b1111'1111'1111; }
std::string print() const;
u64 data = 0;
};
struct GsTexa {
GsTexa() = default;
GsTexa(u64 val) : data(val) {}
u8 ta0() const { return data; }
bool aem() const { return data & (1 << 15); }
u8 ta1() const { return (data >> 32); }
std::string print() const;
u64 data = 0;
};
struct GsFrame {
enum class PSM {
PSMCT32 = 0,
PSMCT24 = 1,
PSMCT16 = 2,
PSMCT16S = 0b1010,
PSMT8 = 0b10011,
PSMT4 = 0b10100,
PSMT8H = 0b011011,
PSMT4HL = 0b100100,
PSMT4HH = 0b101100,
PSMZ32 = 0b110000,
PSMZ24 = 0b110001,
PSMZ16 = 0b110010,
PSMZ16S = 0b111010
};
GsFrame() = default;
GsFrame(u64 val) : data(val) {}
// Frame buffer base pointer (word address / 2048)
u32 fbp() const { return (data & 0b1'1111'1111); }
// Frame buffer width (pixels / 64)
u32 fbw() const { return ((data >> 16) & 0b11'1111); }
// Frame buffer pixel storage format
PSM psm() const { return (PSM)((data >> 24) & 0b11'1111); }
// Frame buffer drawing mask
u32 fbmsk() const { return ((data >> 32) & 0xFFFF'FFFF); }
std::string print() const;
u64 data = 0;
};
struct GsXYOffset {
GsXYOffset() = default;
GsXYOffset(u64 val) : data(val) {}
u32 ofx() const { return data & 0xFFFF; }
u32 ofy() const { return (data >> 32) & 0xFFFF; }
std::string print() const;
u64 data = 0;
};
// not including the giftag
struct AdGifData {
u64 tex0_data;
u64 tex0_addr;
u64 tex1_data;
u64 tex1_addr;
u64 mip_data;
u64 mip_addr;
u64 clamp_data; // can also be zbuf!!
u64 clamp_addr;
u64 alpha_data;
u64 alpha_addr;
bool is_normal_adgif() const {
return (u8)tex0_addr == (u32)GsRegisterAddress::TEX0_1 &&
(u8)tex1_addr == (u32)GsRegisterAddress::TEX1_1 &&
(u8)mip_addr == (u32)GsRegisterAddress::MIPTBP1_1 &&
(u8)clamp_addr == (u32)GsRegisterAddress::CLAMP_1 &&
((u8)alpha_addr == (u32)GsRegisterAddress::ALPHA_1 ||
(u8)alpha_addr == (u32)GsRegisterAddress::MIPTBP2_1);
}
};
static_assert(sizeof(AdGifData) == 5 * 16);
// this represents all of the drawing state, stored as an integer.
// it can also represent "invalid".
class DrawMode {
public:
enum class AlphaBlend {
DISABLED = 0,
SRC_DST_SRC_DST = 1,
SRC_0_SRC_DST = 2,
SRC_0_FIX_DST = 3, // fix = 128
SRC_DST_FIX_DST = 4, // fix = 64
ZERO_SRC_SRC_DST = 5,
SRC_SRC_SRC_SRC = 6,
SRC_0_DST_DST = 7 // Note: requires color_mult tricks
};
enum class AlphaTest {
NEVER = 0,
ALWAYS = 1,
GEQUAL = 2,
};
bool get_depth_write_enable() const { return m_val & 0b1; }
void enable_depth_write() { m_val = m_val | 0b1; }
void disable_depth_write() { m_val = m_val & ~(0b1); }
void set_depth_write_enable(bool x) {
if (x) {
enable_depth_write();
} else {
disable_depth_write();
}
}
GsTest::ZTest get_depth_test() const { return (GsTest::ZTest)((m_val >> 1) & 0b11); }
void set_depth_test(GsTest::ZTest dt) { m_val = (m_val & ~(0b110)) | ((u32)(dt) << 1); }
AlphaBlend get_alpha_blend() const { return (AlphaBlend)((m_val >> 24) & 0b111); }
void set_alpha_blend(AlphaBlend ab) { m_val = (m_val & ~(0b111 << 24)) | ((u32)(ab) << 24); }
u8 get_aref() const { return m_val >> 8; }
void set_aref(u8 val) { m_val = (m_val & ~(0xff00)) | (val << 8); }
AlphaTest get_alpha_test() const { return (AlphaTest)((m_val >> 16) & 0b11); }
void set_alpha_test(AlphaTest ab) { m_val = (m_val & ~(0b11 << 16)) | ((u32)(ab) << 16); }
GsTest::AlphaFail get_alpha_fail() const { return (GsTest::AlphaFail)((m_val >> 21) & 0b11); }
void set_alpha_fail(GsTest::AlphaFail ab) { m_val = (m_val & ~(0b11 << 21)) | ((u32)(ab) << 21); }
bool is_invalid() const { return m_val == UINT32_MAX; }
bool is_valid() const { return !is_invalid(); }
void set_invalid() { m_val = UINT32_MAX; }
bool get_clamp_s_enable() const { return m_val & (1 << 5); }
void set_clamp_s_enable(bool en) {
if (en) {
enable_s_clamp();
} else {
disable_s_clamp();
}
}
void enable_s_clamp() { m_val = m_val | (1 << 5); }
void disable_s_clamp() { m_val = m_val & (~(1 << 5)); }
bool get_filt_enable() const { return m_val & (1 << 6); }
void enable_filt() { m_val = m_val | (1 << 6); }
void disable_filt() { m_val = m_val & (~(1 << 6)); }
void set_filt_enable(bool en) {
if (en) {
enable_filt();
} else {
disable_filt();
}
}
bool get_tcc_enable() const { return m_val & (1 << 6); }
void enable_tcc() { m_val = m_val | (1 << 7); }
void disable_tcc() { m_val = m_val & (~(1 << 7)); }
void set_tcc(bool en) {
if (en) {
enable_tcc();
} else {
disable_tcc();
}
}
bool get_at_enable() const { return m_val & (1 << 18); }
void enable_at() { m_val = m_val | (1 << 18); }
void disable_at() { m_val = m_val & (~(1 << 18)); }
void set_at(bool en) {
if (en) {
enable_at();
} else {
disable_at();
}
}
bool get_zt_enable() const { return m_val & (1 << 19); }
void enable_zt() { m_val = m_val | (1 << 19); }
void disable_zt() { m_val = m_val & (~(1 << 19)); }
void set_zt(bool en) {
if (en) {
enable_zt();
} else {
disable_zt();
}
}
bool get_ab_enable() const { return m_val & (1 << 20); }
void enable_ab() { m_val = m_val | (1 << 20); }
void disable_ab() { m_val = m_val & (~(1 << 20)); }
void set_ab(bool en) {
if (en) {
enable_ab();
} else {
disable_ab();
}
}
bool get_clamp_t_enable() const { return m_val & (1 << 23); }
void set_clamp_t_enable(bool en) {
if (en) {
enable_t_clamp();
} else {
disable_t_clamp();
}
}
void enable_t_clamp() { m_val = m_val | (1 << 23); }
void disable_t_clamp() { m_val = m_val & (~(1 << 23)); }
bool get_decal() const { return !(m_val & (1 << 28)); }
void enable_decal() { m_val = m_val & (~(1 << 28)); }
void disable_decal() { m_val = m_val | (1 << 28); }
void set_decal(bool en) {
if (en) {
enable_decal();
} else {
disable_decal();
}
}
bool get_fog_enable() const { return m_val & (1 << 29); }
void enable_fog() { m_val = m_val | (1 << 29); }
void disable_fog() { m_val = m_val & (~(1 << 29)); }
void set_fog(bool en) {
if (en) {
enable_fog();
} else {
disable_fog();
}
}
u32& as_int() { return m_val; }
const u32& as_int() const { return m_val; }
bool operator==(const DrawMode& other) const { return m_val == other.m_val; }
bool operator!=(const DrawMode& other) const { return m_val != other.m_val; }
std::string to_string() const;
private:
// 0 - depth write enable
// 1, 2 - test: never, always, gequal, greater
// 3, 4 - free
// 5 - clamp enable
// 6 - filt enable
// 7 - tcc enable
// 8,9,10,11,12,14,14,15 - aref
// 16, 17 - atest
// 18 - ate
// 19 - zte
// 20 - abe
// 21, 22 - afail
// 23 t clamp
// 24 - 27 alpha blend
// 28 !decal
// 29 fge
u32 m_val = UINT32_MAX;
};
-185
View File
@@ -1,185 +0,0 @@
# OpenGOAL Formatter Documentation
> [!IMPORTANT]
> This is still in a highly experimental state, adjust your expectations accordingly.
Despite LISP being very simple, and taking the stance of a highly opinionated formatter that doesn't concern itself with line-length in most situations writing a code formatter is still an incredibly annoying and difficult thing to do. This documentation serves as a place for me to collect my thoughts and explain how it works (or more likely, how it does not).
Also in general I found it _really_ hard to find modern and easy to understand articles on how to get started (with the exception of zprint's usage docs, but that doesn't really cover implementation ideas). This area is either dominated by 40 year old journal articles, or discussions around really difficult formatting projects. There isn't a whole lot in the vain of _here's how I made a decent, simple formatter_, so maybe it'll help someone else (even if it's teaching them potentially what not to do!)
## Architecture / Overview
Atleast for me, it helps to understand the path code takes as it flows through the formatting process. Originally I tried to do as much at once for sake of efficiency but this just makes things incredibly hard to reason about and fix bugs. So like any problem that is complex, break it down, the formatter goes through many distinct phases chipping away at the problem:
```mermaid
flowchart TB
subgraph top1 [Build a Formatting Tree]
direction TB
a1(AST Parse) --> a2(Consolidate AST Nodes)
a1 --> a3(Collect Metadata)
end
subgraph top2 [Apply Formatting Configuration]
direction TB
b1(Recursively Iterate each Node) --> b2{Form head has\na predefined config?}
b2 --> |yes|b3(Fetch it and set the config on the node)
b2 --> |no|b4(Apply a default configuration)
end
subgraph top3 [Apply Formatting]
direction TB
c1(For each node, recursively build up a\nlist of source code lines, no indentation) --> c2(Consolidate the first two lines if hanging\nor if the config demands it)
c2 --> c3(Append indentation to each line as appropriate\nand surround each form with relevant parens)
end
subgraph top4 [Finalize Lines]
direction TB
d1(Concatenate all lines with whatever\nfile ending the file started with) --> d2(If we're formatting a file append a trailing new-line\n otherwise we are done)
end
top1-->|FormatterTree|top2
top2-->|FormatterTree with Configurations|top3
top3-->|String List|top4
```
### Build a Formatting Tree
At the end of the day, formatting is about changing the location and amounts of white-space in a file. Meaning it would be ideal if we could start with a representation that is whitespace-agnostic. Some formatters (like cljfmt) choose to use the original whitespace as an input into the formatting process, essentially meaning that your code should already be somewhat-formatted. This is somewhat advantageous as it can simplify the formatter's implementation or be a bit more flexible as to what the original programmer intended (assuming they knew what they were doing). But of course it has one fatal flaw -- the code has to already be well formatted.
For our formatter I wanted it to be able to take in horribly, ugly, minified code and output an optimal result -- especially since so much of our code gets spat out by another tool. To do that, we need to be able to format the code without any existing whitespace helping us, adding in the whitespace from the ground up.
The easiest way to do this is to construct an AST of the language which has a lot of other benefits for building language features but for here all we care about is getting rid of the whitespace and having some basic interpretation of the source code structure. The easiest way now-a-days to make an AST for a language is to create a [treesitter grammar](https://github.com/open-goal/tree-sitter-opengoal).
#### Consolidate AST Nodes and Collect Metadata
One downside with using an AST, atleast with the way the grammar is currently implemented is it creates a somewhat difficult tree to work with from a formatting perspective. For example in a LISP, everything is a list. In our AST it does not look like how you may intuitively expect:
`(+ 1 2)`
```mermaid
flowchart TB
a("source\n(+ 1 2)") --> b("list_lit\n")
b --> c("(") --> ca("(")
b --> d("sym_lit\n+") --> da("+")
b --> e("sym_lit\n1") --> ea("1")
b --> f("sym_lit\n2") --> fa("2")
b --> g(")") --> ga(")")
```
vs what you would probably prefer:
```mermaid
flowchart TB
a("top-level") --> b("list\n- surrounded in ()")
b --> da("+")
b --> ea("1")
b --> fa("2")
```
As you can see from even this simple example, the AST representation can be very verbose and in a sense has duplication. Each node is aware of it's original location in the parsed source-code, but it's the leaf-node tokens that we care about for formatting and perhaps the form types they were a part of (ie. were they comments or in a list, etc).
So to keep things brief, the AST representation is a bit too cumbersome and superfluous to use for formatting directly. So the first step is recurse through it and optimize it, removing unneeded height and capturing crucial metadata that is actually relevant for formatting.
We build a `FormatterTree` which consists of `FormatterNode`s. Each `FormatterNode` has either a single token, or a list of `refs`. A `ref` is just another `FormatterNode`. Each node also has a bunch of metadata and configuration on how it should be formatted (just initialized for now).
Having a dead-simple representation of the code like this is really only possible because it's a LISP, but it's also future proofed for us. If OpenGOAL ever decides to support different brackets -- for example in Clojure a vector uses `[]` or a map uses `{}` it would be easy for update the formatter to wrap each list node in the right bracket type.
### Applying Formatting Configuration
Now that we have our simplified tree, we can get to actually formatting. The first step is to figure out _how_ we intend to format things. By separating this from the actual formatting it's a lot easier to reason about and find bugs. You can also do potentially interesting things, where you do a full traversal of your source-code tree to figure out an optimal decision without trying to build up the final formatted string at the same time.
It is lists, or `form`s that we care about figuring out how to format. Each `form` has a head element (unless it's empty) that we can use to specify how it would be formatted. For example we cannot specify the formatting for every single function name, but we can format a `defun` or a `let` in a special way. With this mechanism, the formatter is easily extensible and configurable. There is even the ability to specify formatting configuration for a list's children by index -- for example you may want to format the first element in a `let` (the binding list) differently than the rest of the code.
There are also some forms that don't have a head-item, that we still identify and set some default formatting rules:
- The top-level node - indentation is disabled
- Lists of only literals, called `constant lists` - These are indented with `1` space instead of the default `2` for a `flow`'d form.
- If these lists literals are paired with keywords we call these `constant pairs` - We strive to put the pairs on the same line, if they are too long we spill to the next with `2` space indentation
- TODO - cond/case pairs (similar to constant pairs)
This brings up two important terms that you will see in the formatter implementation a lot -- `flow` and `hang`. The difference is as such:
```clj
(println "this"
"form"
"is"
"hung / hanged")
```
```clj
(println
"this"
"form"
"is"
"flowed")
```
The difference between the two other than visually is that hanging always results in 1 less line spent _because we don't care about line-length_. This is great because it means we can always default to hanging unless overridden. Flowing trades off that 1 vertical line for horizontal space and looks great for block-style forms. Like the top-level of a `defun` or the content of a `let`.
### Applying Formatting
The final real-step in the process, actually using the configuration and generating the formatting code. This phase is actually broken up into 3 sub-phases as we recurse through the `FormatterTree`
#### Build Lines
First we build the source code lines without indentation, it's a lot easier to figure out the indentation after all tokens are on the correct lines instead of doing it in tandem. Most tokens are given their own line, but in the case of a hung form we combine the first two elements on the first line. Other forms are formatted in even more complicated ways, or comments that were originally inline should remain next to that token.
Secondly, once we have all our consolidated lines we can apply the indentation. This is most often based on the configuration of the list node itself. For example a `hung` form must be indented the width of a `(` and the length of the head-form, plus the whitespace separator -- conversely a `flowed` form only needs to be indented by `2` spaces, regardless of token length. Because all of these algorithms are recursive we can hyper-focus on getting the indentation and formatting correct from the bottom-up. In otherwords, we don't care if a form is 200 spaces indented into a function, once the lines are returned recursively, they will get another layer of indentation added on until we hit the top-level node.
### Finalize Lines
Finally, we concatenate our list of strings, respecting the original file's line-endings.
As it is best practice, if we are formatting an entire file we should terminate with a trailing new-line.
Both of these items are technically a TODO.
## Configuration
TODO
## Reference
This formatter is highly influenced by a lot of other existing formatters and writings. The most significant of which is Clojure's [zprint](https://github.com/kkinnear/zprint/tree/main/doc/options) which has incredible documentation around it's capabilities and why it chooses what to do.
The formatter I've built basically copies zprint in terms of basic functionality, but simplifies things by being even more opinionated (for example, doing far less in terms of trying to adhere to a line-length limit).
### Other references
- [The Hardest Program I’ve Ever Written - Dart Formatter](https://journal.stuffwithstuff.com/2015/09/08/the-hardest-program-ive-ever-written/)
- [cljfmt](https://github.com/weavejester/cljfmt)
### Why not respect a line-length limit
The OpenGOAL formatter will respect line-length in only very specific situations:
- When determining if a form can be fit entirely on a single line
- Probably in the future for multi-line comments
Besides it adding a massive amount of complexity, even zprint explains that a line-length limit won't always be followed
> zprint will work very hard to fit the formatted output into this width, though there are limits to its effort. For instance, it will not reduce the minimum indents in order to satisfy a particular width requirement.
Let's illustrate with an example:
```clj
(defun some-func
(+ 123
456
(+ 123
456
;; imagine this form exceeds our width, how can you "fix" this without
;; messing up the indentation levels of the entire expression
;; - you can't inline it (you're already past the width)
;; - you can't insert a line break because it'll still be at the same horizontal level
(+ 123
456
789))))
```
In LISP code, we don't have many ideal options to deal with this, all options are syntactically correct, but in terms of conventional readability they are bad because when reading LISP code you want to use the indentation as a guide (which is why new-line parens are considered ugly by basically all LISP style guides)
The other reason which is equally relevant is it is hard, it means the formatter would have to try to find an optimal solution by exploring different solutions until it finds the one that minimizes lines.
But with an opinionated formatting style you can avoid this complexity, if you want your code to look nice don't write code like the decompiler sometimes outputs when it encounters something truly cursed:
![](./docs/triangle-of-death.png)
> `game-save.gc` about 200+ nested `let`s!
Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

-510
View File
@@ -1,510 +0,0 @@
#include "formatter.h"
#include "formatter_tree.h"
#include "common/formatter/rules/formatting_rules.h"
#include "common/formatter/rules/rule_config.h"
#include "common/log/log.h"
#include "common/util/FileUtil.h"
#include "common/util/ast_util.h"
#include "common/util/string_util.h"
#include "tree_sitter/api.h"
#include "fmt/core.h"
// Declare the `tree_sitter_opengoal` function, which is
// implemented by the `tree-sitter-opengoal` library.
extern "C" {
extern const TSLanguage* tree_sitter_opengoal();
}
int hang_indentation_width(const FormatterTreeNode& curr_node) {
if (curr_node.token || curr_node.refs.empty()) {
return 0;
}
// Get the first element of the form
const auto& first_elt = curr_node.refs.at(0);
if (first_elt.token) {
return first_elt.token->length() +
2; // +2 because the opening paren and then the following space
}
// Otherwise, continue nesting
return 1 + hang_indentation_width(first_elt);
}
int get_total_form_inlined_width(const FormatterTreeNode& curr_node) {
if (curr_node.token) {
return curr_node.token->length();
}
int width = 1;
for (int i = 0; i < curr_node.refs.size(); i++) {
width += get_total_form_inlined_width(curr_node.refs.at(i));
if (i != curr_node.refs.size() - 1) {
width += 1; // add the space between elements
}
}
return width + 1;
}
// TODO - compute length of each node and store it AOT
void apply_formatting_config(
FormatterTreeNode& curr_node,
std::optional<std::shared_ptr<formatter_rules::config::FormFormattingConfig>>
config_from_parent = {},
std::optional<std::shared_ptr<formatter_rules::config::FormFormattingConfig>>
config_merge_from_parent = {}) {
using namespace formatter_rules;
// node is empty, base-case
if (curr_node.token || curr_node.refs.empty()) {
return;
}
// first, check to see if this form already has a predefined formatting configuration
// if it does, that simplifies things because there is only 1 way of formatting the form
std::optional<formatter_rules::config::FormFormattingConfig> predefined_config;
if (!config_from_parent && !curr_node.refs.empty() && curr_node.refs.at(0).token) {
const auto& form_head = curr_node.refs.at(0).token;
if (form_head && config::opengoal_form_config.find(form_head.value()) !=
config::opengoal_form_config.end()) {
predefined_config = config::opengoal_form_config.at(form_head.value());
curr_node.formatting_config = predefined_config.value();
}
if (config_merge_from_parent) {
const auto& merge_config = *config_merge_from_parent.value();
curr_node.formatting_config.parent_mutable_extra_indent +=
merge_config.parent_mutable_extra_indent;
curr_node.formatting_config.prevent_inlining = merge_config.prevent_inlining;
}
} else if (config_from_parent) {
// TODO - doesn't merge just replaces, a bit inflexible
predefined_config = *config_from_parent.value();
curr_node.formatting_config = predefined_config.value();
}
// In order to keep things simple, as well as because its ineffectual in lisp code (you can only
// enforce it so much without making things unreadable), line width will not matter for deciding
// whether or not to hang or flow the form
//
// This means that a hang would ALWAYS win, because it's 1 less line break. Therefore this
// simplifies our approach there is no need to explore both braches to see which one would be
// preferred.
//
// Instead, we either use the predefined configuration (obviously) or we do some checks for some
// outlier conditions to see if things should be formatted differently
//
// Otherwise, we always default to a hang.
//
// NOTE - any modifications here to child elements could be superseeded later in the recursion!
// In order to maintain your sanity, only modify things here that _arent_ touched by default
// configurations. These are explicitly prepended with `parent_mutable_`
if (!predefined_config && !curr_node.formatting_config.config_set) {
if (curr_node.metadata.is_top_level) {
curr_node.formatting_config.indentation_width = 0;
curr_node.formatting_config.hang_forms = false;
} else if (constant_list::is_constant_list(curr_node)) {
// - Check if the form is a constant list (ie. a list of numbers)
curr_node.formatting_config.indentation_width = 1;
curr_node.formatting_config.hang_forms = false;
curr_node.formatting_config.has_constant_pairs =
constant_pairs::form_should_be_constant_paired(curr_node);
// If applicable, iterate through the constant pairs, since we can potentially pair up
// non-constant second elements in a pair (like a function call), there is the potential that
// they need to spill to the next line and get indented in extra. This is an exceptional
// circumstance, we do NOT do this sort of thing when formatting normal forms (cond/case pairs
// are another similar situation)
if (curr_node.formatting_config.has_constant_pairs) {
for (int i = 0; i < (int)curr_node.refs.size(); i++) {
auto& child_ref = curr_node.refs.at(i);
const auto& type = child_ref.metadata.node_type;
if (constant_types.find(type) == constant_types.end() &&
constant_pairs::is_element_second_in_constant_pair(curr_node, child_ref, i)) {
child_ref.formatting_config.parent_mutable_extra_indent = 2;
}
}
}
} else if (curr_node.formatting_config.hang_forms && curr_node.refs.size() > 1 &&
curr_node.refs.at(1).metadata.is_comment) {
// - Check if the second argument is a comment, it looks better if we flow instead
curr_node.formatting_config.hang_forms = false;
}
}
// If we are hanging, lets determine the indentation width since it is based on the form itself
if (curr_node.formatting_config.hang_forms) {
// TODO - this isn't being calculated for a pre-defined config
// TODO - another idea is to do this during consolidation
curr_node.formatting_config.indentation_width = hang_indentation_width(curr_node);
}
// iterate through the refs
for (int i = 0; i < (int)curr_node.refs.size(); i++) {
auto& ref = curr_node.refs.at(i);
if (!ref.token) {
// If the child has a pre-defined configuration at that index, we pass it along
if (predefined_config && predefined_config->index_config_override.find(i) !=
predefined_config->index_config_override.end()) {
apply_formatting_config(ref, {}, predefined_config->index_config_override.at(i));
} else if (predefined_config && predefined_config->index_configs.find(i) !=
predefined_config->index_configs.end()) {
apply_formatting_config(ref, predefined_config->index_configs.at(i), {});
} else if (predefined_config && predefined_config->default_index_config) {
apply_formatting_config(ref, predefined_config->default_index_config, {});
} else {
apply_formatting_config(ref, {}, {});
}
}
}
// Precompute the column widths for things like deftype fields
if (curr_node.formatting_config.determine_column_widths_for_list_elements) {
// iterate through each ref and find the max length of each index (may be a token, may not be!)
// then store that info in each list element's `list_element_column_widths` to be used when
// printing out the tokens (pad width)
// Find the maximum number of columns
int max_columns = 0;
for (const auto& field : curr_node.refs) {
if ((int)field.refs.size() > max_columns) {
max_columns = field.refs.size();
}
}
// if only one field has a value in the max col position, it looks weird for it to be indented
bool ignore_final_column_width = true;
int fields_with_atleast_max_col = 0;
for (const auto& field : curr_node.refs) {
if ((int)field.refs.size() == max_columns) {
fields_with_atleast_max_col++;
if (fields_with_atleast_max_col > 1) {
ignore_final_column_width = false;
break;
}
}
}
// Now find the column max widths
std::vector<int> column_max_widths = {};
for (int col = 0; col < max_columns; col++) {
column_max_widths.push_back(0);
// -2 because its the indentation before the final column that we want to skip
if (col >= curr_node.formatting_config.num_columns_to_compute_widths ||
(ignore_final_column_width && col == max_columns - 2)) {
continue;
}
for (const auto& field : curr_node.refs) {
if ((int)field.refs.size() > col) {
const auto width = get_total_form_inlined_width(field.refs.at(col));
if (width > column_max_widths.at(col)) {
column_max_widths[col] = width;
}
}
}
}
// Apply column info to every list
for (auto& field : curr_node.refs) {
field.formatting_config.list_element_column_widths = column_max_widths;
field.formatting_config.config_set = true;
}
}
}
bool form_contains_comment(const FormatterTreeNode& curr_node) {
if (curr_node.metadata.is_comment) {
return true;
}
for (const auto& ref : curr_node.refs) {
const auto contains_comment = form_contains_comment(ref);
if (contains_comment) {
return true;
}
}
return false;
}
bool form_contains_node_that_prevents_inlining(const FormatterTreeNode& curr_node) {
if (curr_node.formatting_config.should_prevent_inlining(curr_node.formatting_config,
curr_node.refs.size())) {
return true;
}
for (const auto& ref : curr_node.refs) {
const auto prevents_inlining = form_contains_node_that_prevents_inlining(ref);
if (prevents_inlining) {
return true;
}
}
return false;
}
bool can_node_be_inlined(const FormatterTreeNode& curr_node, int cursor_pos) {
using namespace formatter_rules;
if (curr_node.formatting_config.force_inline) {
// Ensure there are no comments, this still trumps this
if (form_contains_comment(curr_node)) {
return false;
}
return true;
}
// First off, we cannot inline the top level
if (curr_node.metadata.is_top_level) {
return false;
}
// If the config explicitly prevents inlining, or it contains a sub-node that prevents inlining
if (curr_node.formatting_config.prevent_inlining ||
form_contains_node_that_prevents_inlining(curr_node)) {
return false;
}
// nor can we inline something that contains a comment in the middle
if (form_contains_comment(curr_node)) {
return false;
}
// constant pairs are not inlined!
if (curr_node.formatting_config.has_constant_pairs) {
return false;
}
// If this is set in the config, then the form is intended to be partially inlined
if (curr_node.formatting_config.inline_until_index({})) {
return false;
}
// let's see if we can inline the form all on one line to do that, we recursively explore
// the form to find the total width
int line_width = cursor_pos + get_total_form_inlined_width(curr_node);
return line_width <= indent::line_width_target; // TODO - comments
}
std::vector<std::string> apply_formatting(const FormatterTreeNode& curr_node,
std::vector<std::string> /*output*/ = {},
int cursor_pos = 0) {
using namespace formatter_rules;
if (!curr_node.token && curr_node.refs.empty()) {
// special case to handle an empty list
if (curr_node.node_prefix) {
return {fmt::format("{}()", curr_node.node_prefix.value())};
}
return {"()"};
}
// If its a token, just print the token and move on
if (curr_node.token) {
return {curr_node.token_str()};
}
bool inline_form = can_node_be_inlined(curr_node, cursor_pos);
// TODO - also if the form is inlinable, we can skip all the complication below and just...inline
// it!
// TODO - should figure out the inlining here as well, instead of the bool above
// Iterate the form, building up a list of the final lines but don't worry about indentation
// at this stage. Once the lines are finalized, it's easy to add the indentation later
//
// This means we may combine elements onto the same line in this step.
std::vector<std::string> form_lines = {};
for (int i = 0; i < (int)curr_node.refs.size(); i++) {
const auto& ref = curr_node.refs.at(i);
// Add new line entry
if (ref.token) {
// Cleanup block-comments
if (ref.metadata.node_type == "block_comment") {
const auto comment_lines = comments::format_block_comment(ref.token_str());
for (const auto& line : comment_lines) {
form_lines.push_back(line);
}
} else {
form_lines.push_back(ref.token_str());
}
if (!curr_node.metadata.is_top_level && i == (int)curr_node.refs.size() - 1 &&
(ref.metadata.is_comment)) {
// if there's an inline comment at the end of a form, we have to force the paren to the next
// line and do a new-line paren this is ugly, but we have no choice!
form_lines.push_back("");
}
} else {
// If it's not a token, we have to recursively build up the form
// TODO - add the cursor_pos here
const auto& lines = apply_formatting(ref, {}, cursor_pos);
for (int i = 0; i < (int)lines.size(); i++) {
const auto& line = lines.at(i);
form_lines.push_back(fmt::format(
"{}{}", str_util::repeat(ref.formatting_config.parent_mutable_extra_indent, " "),
line));
}
}
// If we are hanging forms, combine the first two forms onto the same line
if (i == (int)curr_node.refs.size() - 1 && form_lines.size() > 1 &&
(curr_node.formatting_config.hang_forms ||
curr_node.formatting_config.combine_first_two_lines)) {
form_lines.at(0) += fmt::format(" {}", str_util::ltrim(form_lines.at(1)));
form_lines.erase(form_lines.begin() + 1);
} else if ((i + 1) < (int)curr_node.refs.size()) {
const auto& next_ref = curr_node.refs.at(i + 1);
// combine the next inline comment or constant pair
if ((next_ref.metadata.node_type == "comment" && next_ref.metadata.is_inline) ||
(curr_node.formatting_config.has_constant_pairs &&
constant_pairs::is_element_second_in_constant_pair(curr_node, next_ref, i + 1)) ||
constant_pairs::is_element_second_in_constant_pair_new(curr_node.refs.at(i), next_ref)) {
// TODO
// has issues with not consolidating first lines, this should probably just be moved to
// outside this loop for simplicity, do it later
if (next_ref.token) {
form_lines.at(form_lines.size() - 1) += fmt::format(" {}", next_ref.token_str());
i++;
// We have to handle hang-consolidation here or else it will never be reached above!
if (i == (int)curr_node.refs.size() - 1 && form_lines.size() > 1 &&
(curr_node.formatting_config.hang_forms ||
curr_node.formatting_config.combine_first_two_lines)) {
form_lines.at(0) += fmt::format(" {}", form_lines.at(1));
form_lines.erase(form_lines.begin() + 1);
}
} else if (can_node_be_inlined(next_ref, cursor_pos)) {
const auto& lines = apply_formatting(next_ref, {}, cursor_pos); // TODO - cursor pos
for (const auto& line : lines) {
form_lines.at(form_lines.size() - 1) += fmt::format(" {}", line);
}
i++;
// We have to handle hang-consolidation here or else it will never be reached above!
if (i == (int)curr_node.refs.size() - 1 && form_lines.size() > 1 &&
(curr_node.formatting_config.hang_forms ||
curr_node.formatting_config.combine_first_two_lines)) {
form_lines.at(0) += fmt::format(" {}", form_lines.at(1));
form_lines.erase(form_lines.begin() + 1);
}
}
if (!curr_node.metadata.is_top_level && next_ref.metadata.node_type == "comment" &&
(i + 1) == (int)curr_node.refs.size()) {
form_lines.push_back("");
}
}
}
// If we are at the top level, potential separate with a new line
if (blank_lines::should_insert_blank_line(curr_node, ref, i)) {
form_lines.at(form_lines.size() - 1) += "\n";
}
}
// Consolidate any lines if the configuration requires it
// TODO there is a hack here so that multi-line forms that are consolidated still line up properly
// i have to make consolidate a more first-class feature of the config
// TODO - hacky, but prevents a bad situation, clean up
if (curr_node.formatting_config.inline_until_index(form_lines) &&
!str_util::contains(form_lines.at(0), ";")) {
std::vector<std::string> new_form_lines = {};
const auto original_form_head_width = str_util::split(form_lines.at(0), '\n').at(0).length();
bool consolidating_lines = true;
for (int i = 0; i < (int)form_lines.size(); i++) {
if (i < curr_node.formatting_config.inline_until_index(form_lines)) {
if (new_form_lines.empty()) {
new_form_lines.push_back(form_lines.at(i));
} else {
new_form_lines.at(0) += fmt::format(" {}", form_lines.at(i));
}
} else {
if (str_util::starts_with(form_lines.at(i), " ") && consolidating_lines) {
new_form_lines.push_back(fmt::format(
"{}{}", str_util::repeat(original_form_head_width, " "), form_lines.at(i)));
} else {
consolidating_lines = false;
new_form_lines.push_back(form_lines.at(i));
}
}
}
form_lines = new_form_lines;
}
// Add any column padding
if (!curr_node.formatting_config.list_element_column_widths.empty()) {
for (int i = 0; i < (int)form_lines.size(); i++) {
const auto& token = form_lines.at(i);
if (i < (int)form_lines.size() - 1) {
form_lines[i] = str_util::pad_right(
token, curr_node.formatting_config.list_element_column_widths.at(i), ' ');
}
}
}
// Apply necessary indentation to each line and add parens
if (!curr_node.metadata.is_top_level) {
std::string form_surround_start = "(";
if (curr_node.node_prefix) {
form_surround_start = fmt::format("{}(", curr_node.node_prefix.value());
}
std::string form_surround_end = ")";
form_lines[0] = fmt::format("{}{}", form_surround_start, form_lines[0]);
form_lines[form_lines.size() - 1] =
fmt::format("{}{}", form_lines[form_lines.size() - 1], form_surround_end);
}
if (inline_form) {
// NOTE - not sure about this, if we are inlining a form, it always makes sense to eliminate
// trailing whitespace the only issue i can foresee is related to strings that span multiple
// lines.
std::vector<std::string> new_form_lines = {};
for (const auto& form_line : form_lines) {
new_form_lines.push_back(str_util::ltrim(form_line));
}
form_lines = {fmt::format("{}", fmt::join(new_form_lines, " "))};
} else {
bool currently_in_block_comment = false;
for (int i = 0; i < (int)form_lines.size(); i++) {
auto& line = form_lines.at(i);
if (str_util::contains(line, "|#")) {
currently_in_block_comment = false;
}
if (i > 0 && !currently_in_block_comment) {
line = fmt::format("{}{}",
str_util::repeat(curr_node.formatting_config.indentation_width_for_index(
curr_node.formatting_config, i),
" "),
line);
}
if (str_util::contains(line, "#|") && !str_util::contains(line, "|#")) {
currently_in_block_comment = true;
}
}
}
return form_lines;
}
std::string join_formatted_lines(const std::vector<std::string>& lines,
const std::string& line_ending) {
return fmt::format("{}", fmt::join(lines, line_ending));
}
std::optional<std::string> formatter::format_code(const std::string& source) {
// Create a parser.
std::shared_ptr<TSParser> parser(ts_parser_new(), TreeSitterParserDeleter());
ts_parser_set_language(parser.get(), tree_sitter_opengoal());
// Build a syntax tree based on source code stored in a string.
std::shared_ptr<TSTree> tree(
ts_parser_parse_string(parser.get(), NULL, source.c_str(), source.length()),
TreeSitterTreeDeleter());
// Get the root node of the syntax tree.
TSNode root_node = ts_tree_root_node(tree.get());
if (ts_node_is_null(root_node)) {
lg::error("null root node");
return std::nullopt;
} else if (ts_node_has_error(root_node)) {
lg::error("grammar parsing error, go figure it out!");
return std::nullopt;
}
try {
// There are three phases of formatting
// 1. Simplify the AST down to something that is easier to work on from a formatting perspective
// this also gathers basic metadata that can be done at this stage, like if the token is a
// comment or if the form is on the top-level
auto formatting_tree = FormatterTree(source, root_node);
// 2. Recursively iterate through this simplified FormatterTree and figure out what rules
// need to be applied to produce an optimal result
apply_formatting_config(formatting_tree.root);
// 3. Use this updated FormatterTree to print out the final source-code, while doing so
// we may deviate from the optimal result to produce something even more optimal by inlining
// forms that can fit within the line width.
const auto formatted_lines = apply_formatting(formatting_tree.root);
// 4. Now we joint he lines together, it's easier when formatting to leave all lines independent
// so adding indentation is easier
const auto formatted_source =
join_formatted_lines(formatted_lines, file_util::get_majority_file_line_endings(source));
return formatted_source;
} catch (std::exception& e) {
lg::error("Unable to format code - {}", e.what());
}
return std::nullopt;
}
-19
View File
@@ -1,19 +0,0 @@
#pragma once
#include <optional>
#include <string>
#include "tree_sitter/api.h"
namespace formatter {
struct TreeSitterParserDeleter {
void operator()(TSParser* ptr) const { ts_parser_delete(ptr); }
};
struct TreeSitterTreeDeleter {
void operator()(TSTree* ptr) const { ts_tree_delete(ptr); }
};
std::optional<std::string> format_code(const std::string& source);
} // namespace formatter
-177
View File
@@ -1,177 +0,0 @@
#include "formatter_tree.h"
#include "common/util/string_util.h"
#include "fmt/core.h"
std::string get_source_code(const std::string& source, const TSNode& node) {
uint32_t start = ts_node_start_byte(node);
uint32_t end = ts_node_end_byte(node);
return source.substr(start, end - start);
}
int num_blank_lines_following_node(const std::string& source, const TSNode& node) {
int num_lines = -1; // The first new-line encountered is not a blank line
uint32_t cursor = ts_node_end_byte(node);
// TODO - this breaks on lines with whitespace as well, should probably seek past that!
while (cursor < source.length() && source.at(cursor) == '\n') {
num_lines++;
cursor++;
}
return num_lines;
}
// Check if the original source only has whitespace up to a new-line before it's token
bool node_preceeded_by_only_whitespace(const std::string& source, const TSNode& node) {
// NOTE - this returns incorrectly because we skip brackets in lists, we'll see if that matters
int32_t pos = ts_node_start_byte(node) - 1;
while (pos > 0) {
const auto& c = source.at(pos);
if (c == '\n') {
return true;
} else if (c == ' ' || c == '\t') {
pos--;
continue;
}
return false;
}
return true;
}
FormatterTreeNode::FormatterTreeNode(const std::string& source, const TSNode& node)
: token(get_source_code(source, node)) {
metadata.node_type = ts_node_type(node);
metadata.is_comment = str_util::starts_with(str_util::ltrim(token.value()), ";") ||
str_util::starts_with(str_util::ltrim(token.value()), "#|");
// Set any metadata based on the value of the token
metadata.num_blank_lines_following = num_blank_lines_following_node(source, node);
metadata.is_inline = !node_preceeded_by_only_whitespace(source, node);
};
// Check if the original source only has whitespace up to a new-line after it's token
bool node_followed_by_only_whitespace(const std::string& source, const TSNode& node) {
uint32_t pos = ts_node_end_byte(node);
while (pos < source.length()) {
const auto& c = source.at(pos);
if (c == '\n') {
return true;
} else if (c == ' ' || c == '\t') {
pos++;
continue;
}
return false;
}
return true;
}
bool nodes_on_same_line(const std::string& source, const TSNode& n1, const TSNode& n2) {
// Get the source between the two lines, if there are any new-lines, the answer is NO
uint32_t start = ts_node_start_byte(n1);
uint32_t end = ts_node_end_byte(n2);
const auto code_between = source.substr(start, end - start);
return !str_util::contains(code_between, "\n");
}
FormatterTree::FormatterTree(const std::string& source, const TSNode& root_node) {
root = FormatterTreeNode();
root.metadata.is_top_level = true;
construct_formatter_tree_recursive(source, root_node, root);
}
const std::unordered_map<std::string, std::vector<std::string>> node_type_ignorable_contents = {
{"list_lit", {"(", ")"}}};
// TODO make an imperative version eventually
// TODO - cleanup duplication
void FormatterTree::construct_formatter_tree_recursive(const std::string& source,
TSNode curr_node,
FormatterTreeNode& tree_node,
std::optional<std::string> node_prefix) {
if (ts_node_child_count(curr_node) == 0) {
auto new_node = FormatterTreeNode(source, curr_node);
new_node.node_prefix = node_prefix;
tree_node.refs.push_back(new_node);
return;
}
const std::string curr_node_type = ts_node_type(curr_node);
FormatterTreeNode list_node;
std::optional<std::string> next_node_prefix;
if (curr_node_type == "list_lit") {
list_node = FormatterTreeNode();
} else if (curr_node_type == "str_lit") {
// Strings are a special case, they are literals and essentially tokens but the grammar can
// detect formatter identifiers, this is useful for semantic highlighting but doesn't matter for
// formatting So for strings, we treat them as if they should be a single token
tree_node.refs.push_back(FormatterTreeNode(source, curr_node));
return;
} else if (curr_node_type == "quoting_lit") {
if (node_prefix) {
node_prefix.value() += "'";
} else {
node_prefix = "'";
}
construct_formatter_tree_recursive(source, ts_node_child(curr_node, 1), tree_node, node_prefix);
return;
} else if (curr_node_type == "unquoting_lit") {
if (node_prefix) {
node_prefix.value() += ",";
} else {
node_prefix = ",";
}
construct_formatter_tree_recursive(source, ts_node_child(curr_node, 1), tree_node, node_prefix);
return;
} else if (curr_node_type == "quasi_quoting_lit") {
if (node_prefix) {
node_prefix.value() += "`";
} else {
node_prefix = "`";
}
construct_formatter_tree_recursive(source, ts_node_child(curr_node, 1), tree_node, node_prefix);
return;
} else if (curr_node_type == "unquote_splicing_lit") {
if (node_prefix) {
node_prefix.value() += ",@";
} else {
node_prefix = ",@";
}
construct_formatter_tree_recursive(source, ts_node_child(curr_node, 1), tree_node, node_prefix);
return;
}
std::vector<std::string> skippable_nodes = {};
if (node_type_ignorable_contents.find(curr_node_type) != node_type_ignorable_contents.end()) {
skippable_nodes = node_type_ignorable_contents.at(curr_node_type);
}
for (size_t i = 0; i < ts_node_child_count(curr_node); i++) {
const auto child_node = ts_node_child(curr_node, i);
auto debug_child = ts_node_string(child_node);
const auto contents = get_source_code(source, child_node);
bool skip_node = false;
for (const auto& skippable_content : skippable_nodes) {
if (skippable_content == contents) {
skip_node = true;
break;
}
}
if (skip_node) {
continue;
}
if (curr_node_type == "list_lit") {
construct_formatter_tree_recursive(source, child_node, list_node, {});
if (node_prefix) {
list_node.node_prefix = node_prefix;
}
} else {
construct_formatter_tree_recursive(source, child_node, tree_node, node_prefix);
if (node_prefix && !tree_node.refs.empty()) {
tree_node.refs.at(tree_node.refs.size() - 1).node_prefix = node_prefix;
}
}
}
if (curr_node_type == "list_lit") {
// special case for empty lists
if (node_prefix && !list_node.node_prefix) {
list_node.node_prefix = node_prefix;
}
tree_node.refs.push_back(list_node);
}
}
-78
View File
@@ -1,78 +0,0 @@
#pragma once
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include "rules/rule_config.h"
#include "tree_sitter/api.h"
// Treesitter is fantastic for validating and parsing our code into a structured tree format without
// whitespace so we can do that ourselves (formatting) However, the treesitter AST is a bit too
// detailed for purposes of formatting.
//
// When formatting there is no need to know things like are we
// in a function, or a symbol, etc. This extra information is fantastic for parsing or manipulating
// the language, but becomes burdensome when just trying to write elegant formatting code when all
// we really care about is:
// - getting all the text tokens for the source code
// - having them in a proper, nested format
//
// TLDR - The treesitter format is complicated and highly nested, leading to some very hard to
// understand code. So my solution is atleast a 2-pass format.
//
// Pass 1 - convert the AST into a simplified FormatterTree
// Pass 2 - use the simplified tree to output the final code
class FormatterTreeNode {
public:
struct Metadata {
std::string node_type;
bool is_top_level = false;
bool is_comment = false;
bool is_inline = false;
int num_blank_lines_following = 0;
bool is_binding_list = false; // TODO set this
};
std::vector<FormatterTreeNode> refs;
Metadata metadata;
// The token is optional because list nodes do not contain a token, they just contain a bunch of
// eventually-containing token node refs
std::optional<std::string> token;
std::optional<std::string> node_prefix;
formatter_rules::config::FormFormattingConfig formatting_config;
FormatterTreeNode() = default;
FormatterTreeNode(const std::string& source, const TSNode& node);
FormatterTreeNode(const Metadata& _metadata) : metadata(_metadata){};
bool is_list() const { return !token.has_value(); }
std::string token_str() const {
if (node_prefix && token) {
return node_prefix.value() + token.value();
}
if (token) {
return token.value();
}
return "";
}
};
// A FormatterTree has a very simple and crude tree structure where:
// - Nodes are essentially forms, which contain in-order tokens or references to nested forms
// - Nodes can have associated metadata, often related to their context in the original code
// - Nodes can also have multiple formatting rules associated with them. Often this is the default
// rule or based on pre-configured overrides due to the head of the form, ex. 'defun'
class FormatterTree {
public:
FormatterTree(const std::string& source, const TSNode& root_node);
FormatterTreeNode root;
private:
void construct_formatter_tree_recursive(const std::string& source,
TSNode curr_node,
FormatterTreeNode& tree_node,
std::optional<std::string> node_prefix = {});
};
-191
View File
@@ -1,191 +0,0 @@
#include "formatting_rules.h"
#include <set>
#include "common/util/string_util.h"
#include "fmt/core.h"
namespace formatter_rules {
// TODO - probably need to include quoted literals as well, though the grammar currently does not
// differentiate between a quoted symbol and a quoted form
const std::set<std::string> constant_types = {"kwd_lit", "num_lit", "str_lit",
"char_lit", "null_lit", "bool_lit"};
const std::set<std::string> constant_type_forms = {"meters", "seconds", "degrees"};
namespace constant_list {
bool is_constant_list(const FormatterTreeNode& node) {
if (!node.is_list() || node.refs.empty()) {
return false;
}
if (!node.refs.at(0).token) {
return true;
}
const auto& type = node.refs.at(0).metadata.node_type;
return constant_types.find(type) != constant_types.end();
}
} // namespace constant_list
namespace blank_lines {
bool should_insert_blank_line(const FormatterTreeNode& containing_node,
const FormatterTreeNode& node,
const int index) {
// We only do this at the top level and don't leave a trailing new-line
if (!containing_node.metadata.is_top_level || index >= (int)containing_node.refs.size() - 1) {
return false;
}
// If it's a comment, but has no following blank lines, dont insert a blank line
if (node.metadata.is_comment && node.metadata.num_blank_lines_following == 0) {
return false;
}
// If the next form is a comment and is inline, don't insert a new line
if ((index + 1) < (int)containing_node.refs.size() &&
containing_node.refs.at(index + 1).metadata.is_comment &&
containing_node.refs.at(index + 1).metadata.is_inline) {
return false;
}
if (node.formatting_config.elide_top_level_newline) {
if ((index + 1) < (int)containing_node.refs.size() &&
containing_node.refs.at(index + 1).metadata.is_comment) {
return true;
}
return false;
}
return true;
}
} // namespace blank_lines
namespace comments {
std::vector<std::string> format_block_comment(const std::string& comment) {
// Normalize block comments, remove any trailing or leading whitespace
// Only allow annotations on the first line, like #|@file
// Don't mess with internal indentation as the user might intend it to be a certain way.
std::string new_comment = "";
std::string comment_contents = "";
bool seek_until_whitespace = str_util::starts_with(comment, "#|@");
int chars_seeked = 0;
for (const auto& c : comment) {
if (c == '\n' || (seek_until_whitespace && (c == ' ' || c == '\t')) ||
(!seek_until_whitespace && (c != '#' && c != '|'))) {
break;
}
chars_seeked++;
new_comment += c;
}
// Remove the first line content and any leading whitespace
comment_contents = str_util::ltrim_newlines(comment.substr(chars_seeked));
// Remove trailing whitespace
comment_contents = str_util::rtrim(comment_contents);
// remove |#
if (str_util::ends_with(comment_contents, "|#")) {
comment_contents.pop_back();
comment_contents.pop_back();
}
comment_contents = str_util::rtrim(comment_contents);
std::vector<std::string> lines = {new_comment};
const auto contents_as_lines = str_util::split_string(comment_contents, "\n");
if (contents_as_lines.size() > 1) {
for (const auto& line : contents_as_lines) {
lines.push_back(line);
}
lines.push_back("|#");
} else {
lines.at(0) = fmt::format("{} {} |#", new_comment, str_util::trim(contents_as_lines.at(0)));
}
return lines;
}
} // namespace comments
namespace constant_pairs {
// TODO - remove index, not needed, could just pass in the previous node
bool is_element_second_in_constant_pair(const FormatterTreeNode& containing_node,
const FormatterTreeNode& node,
const int index) {
if (containing_node.refs.empty() || index == 0) {
return false;
}
// Ensure that a keyword came before hand
if (containing_node.refs.at(index - 1).metadata.node_type != "kwd_lit") {
return false;
} else if (node.metadata.node_type == "kwd_lit") {
// NOTE - there is ambiugity here which cannot be totally solved (i think?)
// if the element itself is also a keyword, assume this is two adjacent keywords and they should
// not be paired
return false;
}
return true;
}
// TODO - potentially remove the above
bool is_element_second_in_constant_pair_new(const FormatterTreeNode& prev_node,
const FormatterTreeNode& curr_node) {
if (prev_node.metadata.node_type == "kwd_lit") {
// Handle standard constant types
// TODO - pair up sym_names as well
if (constant_types.find(curr_node.metadata.node_type) != constant_types.end()) {
if (curr_node.metadata.node_type != "kwd_lit") {
// NOTE - there is ambiugity here which cannot be totally solved (i think?)
// if the element itself is also a keyword, assume this is two adjacent keywords and they
// should not be paired
return true;
}
}
// Quoted symbols
if (curr_node.metadata.node_type == "sym_name" && curr_node.node_prefix &&
(curr_node.node_prefix.value() == "'" || curr_node.node_prefix.value() == ",")) {
return true;
}
if (!curr_node.refs.empty()) {
// Constant forms special cases (ie. meters)
if (constant_type_forms.find(curr_node.refs.at(0).token_str()) != constant_type_forms.end()) {
return true;
}
// If they are just a list of symbol names (enum or simple method call)
bool all_symbols = true;
for (const auto& ref : curr_node.refs) {
if (ref.metadata.node_type != "sym_name") {
all_symbols = false;
break;
}
}
if (all_symbols) {
return true;
}
}
}
return false;
}
bool form_should_be_constant_paired(const FormatterTreeNode& node) {
// Criteria for a list to be constant paired:
// - needs to start with a non-symbol
// - needs atleast the minimum amount of pairs, so 2 pairs can still be inlined
if (node.refs.empty()) {
return false;
}
int num_pairs = 0;
for (int i = 0; i < (int)node.refs.size() - 1; i++) {
const auto& ref = node.refs.at(i);
const auto& next_ref = node.refs.at(i + 1);
if (ref.token && next_ref.token) {
// If the first element a keyword and the following item is a constant, it's a pair
// move forward one extra index
if (ref.metadata.node_type == "kwd_lit" &&
constant_types.find(next_ref.metadata.node_type) != constant_types.end()) {
num_pairs++;
i++;
}
}
}
return num_pairs >= min_pair_amount;
}
} // namespace constant_pairs
} // namespace formatter_rules
-121
View File
@@ -1,121 +0,0 @@
#pragma once
#include <set>
#include <string>
#include "common/formatter/formatter_tree.h"
namespace formatter_rules {
extern const std::set<std::string> constant_types;
namespace constant_list {
bool is_constant_list(const FormatterTreeNode& node);
}
// The formatter will try to collapse as much space as possible in the top-level, this means
// separating forms by a single empty blank line
//
// The exception are comments, top level comments will retain their following blank lines from the
// original source
// - this could be none, in the case where a comment is directly next to a form (like this one!)
// - you don't want them to be separated!
// - or this could be top level comments / comment blocks documenting things but not being
// associated with a form
// - in this case, you want them to remain separated
//
// Reference - https://github.com/kkinnear/zprint/blob/main/doc/options/blank.md
namespace blank_lines {
bool should_insert_blank_line(const FormatterTreeNode& containing_node,
const FormatterTreeNode& node,
const int index);
} // namespace blank_lines
// TODO:
// - align consecutive comment lines
// - if/when the formatter is concerned with line length, there are implications here
//
// Reference - https://github.com/kkinnear/zprint/blob/main/doc/options/comments.md
namespace comments {
std::vector<std::string> format_block_comment(const std::string& comment);
}
// Paired elements in a list will be kept in-line rather than the default new-line indentation
// For example:
// (:msg "hello world" :delay 100 :fn (lambda () (+ 1 1)))
// Would typically become:
// (:msg
// "hello world"
// :delay
// 100
// :fn
// (lambda ()
// (+ 1 1)))
// But with constant pairs:
// (:msg "hello world"
// :delay 100
// :fn
// (lambda ()
// (+ 1 1)))
//
// Reference - https://github.com/kkinnear/zprint/blob/main/doc/options/constantpairs.md
namespace constant_pairs {
const static int min_pair_amount = 4;
// Determines if the given element is the second element in a constant pair, if it is then we would
// usually want to elide the new-line in whatever code that applies it
//
// This is true if:
// - the element is in a list
// - the element is preceeded by a keyword
// - the element is a:
// - keyword, symbol, string, number, or boolean
bool is_element_second_in_constant_pair(const FormatterTreeNode& containing_node,
const FormatterTreeNode& node,
const int index);
bool is_element_second_in_constant_pair_new(const FormatterTreeNode& prev_node,
const FormatterTreeNode& curr_node);
bool form_should_be_constant_paired(const FormatterTreeNode& node);
} // namespace constant_pairs
// There are two main types of indentations "flow"s and "hang"s
// (this is
// a
// hang)
// (this
// is
// a
// flow)
//
// `flow` is the default indentation mode and right now the determination between the two is down to
// manual configuration based on the head of the form. In general, we only `flow` or `hang` if the
// form cannot fit on the current line.
//
// Additionally, if the head of the form is a constant we `flow` with an indent of `1` instead of
// `2`
//
// By default, we always hang unless:
// - the head-form overrides the configuration
// - TODO it will use more lines than the flow approach
//
// Reference - https://github.com/kkinnear/zprint/blob/main/doc/options/indent.md
namespace indent {
const static int line_width_target = 120;
} // namespace indent
// Let forms fall into two main categories
// - Ones that can be entirely inlined
// - Ones that cannot
//
// Single line let forms:
// - If there is a single binding, let it continue to be one line
// - otherwise, flow it just as a multi-line let would be done
//
// For multi-line let forms, the difficulty is how to format the binding:
// - align the bindings to within the binding list (single indent space)
// - always format the binding value on the same line as the binding, no new lines
// - forms inside the let binding are flowed
//
// Reference - https://github.com/kkinnear/zprint/blob/main/doc/options/let.md
namespace let {} // namespace let
} // namespace formatter_rules
-304
View File
@@ -1,304 +0,0 @@
#include "rule_config.h"
#include "common/util/string_util.h"
namespace formatter_rules {
namespace config {
static FormFormattingConfig new_inlinable_simple_flow_rule() {
return {.config_set = true, .hang_forms = false};
}
static FormFormattingConfig new_permissive_flow_rule() {
return {.config_set = true, .hang_forms = false, .combine_first_two_lines = true};
}
static FormFormattingConfig new_flow_rule(int start_index, bool has_constant_pairs = false) {
return {.config_set = true,
.hang_forms = false,
.inline_until_index =
[start_index](const std::vector<std::string>& /*curr_lines*/) { return start_index; },
.has_constant_pairs = has_constant_pairs};
}
static FormFormattingConfig new_function_rule(int start_index, bool has_constant_pairs = false) {
FormFormattingConfig cfg = {
.config_set = true,
.hang_forms = false,
.inline_until_index =
[start_index](const std::vector<std::string>& /*curr_lines*/) { return start_index; },
.has_constant_pairs = has_constant_pairs};
auto arg_list_config = std::make_shared<FormFormattingConfig>();
arg_list_config->force_inline = true;
arg_list_config->hang_forms = false;
cfg.index_configs.emplace(2, arg_list_config);
return cfg;
}
static FormFormattingConfig new_inlineable_flow_rule(int start_index,
bool has_constant_pairs = false) {
return {.config_set = true,
.hang_forms = false,
.inline_until_index =
[start_index](const std::vector<std::string>& curr_lines) {
int total_width = 0;
for (const auto& line : curr_lines) {
// Check for comments
// TODO - this shows how this isn't really the best strategy but it holds up
if (str_util::contains(line, ";")) {
// Can't inline, there's a comment!
return start_index;
}
total_width += line.length();
// an empty line implies a new-line was forced, this is bleeding implementation
// details, but fine for now
if (line.empty()) {
return start_index;
}
}
if (total_width <= 120) {
return (int)curr_lines.size();
}
return start_index;
},
.has_constant_pairs = has_constant_pairs};
}
static FormFormattingConfig new_defstate_rule(int start_index, bool has_constant_pairs = false) {
FormFormattingConfig cfg = {
.config_set = true,
.hang_forms = false,
.inline_until_index =
[start_index](const std::vector<std::string>& /*curr_lines*/) { return start_index; },
.has_constant_pairs = has_constant_pairs};
std::vector<int> state_handler_indexes = {4, 6, 8, 10,
12, 14}; // NOTE - not all of these have to be defined
for (const auto& index : state_handler_indexes) {
auto temp_config = std::make_shared<FormFormattingConfig>();
temp_config->prevent_inlining = true;
temp_config->parent_mutable_extra_indent = 2;
cfg.index_config_override.emplace(index, temp_config);
}
return cfg;
}
static FormFormattingConfig new_defmethod_rule(int start_index, bool has_constant_pairs = false) {
// TODO - might be nice to have a function that returns a config based on a given index, instead
// of hardcoding them!
// Right now this only works for non-`new` methods (else we may bleed into the body of a normal
// method)
auto arg_list_config = std::make_shared<FormFormattingConfig>();
arg_list_config->force_inline = true;
arg_list_config->hang_forms = false;
FormFormattingConfig cfg = {.config_set = true,
.hang_forms = false,
.inline_until_index =
[start_index](const std::vector<std::string>& curr_lines) {
if (curr_lines.size() >= 2 && curr_lines.at(1) == "new") {
// defmethod was changed to omit the type name for everything
// except the `new` method, so special case.
return start_index + 1;
}
return start_index;
},
.has_constant_pairs = has_constant_pairs};
cfg.index_configs.emplace(2, arg_list_config);
return cfg;
}
static FormFormattingConfig new_lambda_rule(int start_index, bool has_constant_pairs = false) {
FormFormattingConfig cfg = {.config_set = true,
.hang_forms = false,
.inline_until_index =
[start_index](const std::vector<std::string>& curr_lines) {
if (curr_lines.size() >= 2 && curr_lines.at(1) == ":behavior") {
// defmethod was changed to omit the type name for everything
// except the `new` method, so special case.
return start_index + 2;
}
return start_index;
},
.has_constant_pairs = has_constant_pairs};
return cfg;
}
static FormFormattingConfig new_defenum_rule() {
auto temp_list_config = std::make_shared<FormFormattingConfig>();
temp_list_config->force_inline = true;
temp_list_config->hang_forms = false;
return {
.config_set = true,
.hang_forms = false,
.inline_until_index = [](const std::vector<std::string>& /*curr_lines*/) { return 2; },
.has_constant_pairs = true,
.default_index_config = temp_list_config,
};
}
static FormFormattingConfig new_deftype_rule(
int start_index,
int num_columns_to_compute_widths,
const std::vector<int>& inlining_preventation_indices) {
FormFormattingConfig cfg;
cfg.has_constant_pairs = true;
cfg.config_set = true;
cfg.hang_forms = false;
cfg.inline_until_index = [start_index](std::vector<std::string> curr_lines) {
if (curr_lines.size() >= 4 && curr_lines.at(3) == "()") {
return 4;
}
return start_index;
};
for (const auto& index : inlining_preventation_indices) {
auto temp_config = std::make_shared<FormFormattingConfig>();
temp_config->config_set = true;
temp_config->prevent_inlining = true;
temp_config->hang_forms = false;
temp_config->indentation_width = 1;
auto temp_list_config = std::make_shared<FormFormattingConfig>();
temp_list_config->force_inline = true;
temp_list_config->hang_forms = false;
temp_config->default_index_config = temp_list_config;
if (index == 3) {
temp_config->determine_column_widths_for_list_elements = true;
temp_config->num_columns_to_compute_widths = num_columns_to_compute_widths;
}
cfg.index_configs.emplace(index, temp_config);
}
return cfg;
}
static FormFormattingConfig new_binding_rule(int form_head_width) {
FormFormattingConfig cfg;
cfg.config_set = true;
cfg.hang_forms = false;
cfg.combine_first_two_lines = true;
auto binding_list_config = std::make_shared<FormFormattingConfig>();
binding_list_config->config_set = true;
binding_list_config->hang_forms = false;
binding_list_config->indentation_width = 1;
binding_list_config->indentation_width_for_index = [form_head_width](FormFormattingConfig /*cfg*/,
int index) {
if (index == 0) {
return 0;
}
return form_head_width;
};
binding_list_config->should_prevent_inlining = [](FormFormattingConfig /*config*/, int num_refs) {
// Only prevent inlining a binding list, if there are more than 1 bindings
if (num_refs > 1) {
return true;
}
return false;
};
binding_list_config->prevent_inlining =
true; // TODO - we only want to prevent inlining if there are more than 2 elements
cfg.index_configs.emplace(1, binding_list_config);
return cfg;
}
static FormFormattingConfig new_inline_binding_rule(int form_head_width) {
FormFormattingConfig cfg;
cfg.config_set = true;
cfg.hang_forms = false;
cfg.combine_first_two_lines = true;
auto binding_list_config = std::make_shared<FormFormattingConfig>();
binding_list_config->config_set = true;
binding_list_config->hang_forms = false;
binding_list_config->indentation_width = 1;
binding_list_config->indentation_width_for_index = [form_head_width](FormFormattingConfig /*cfg*/,
int index) {
if (index == 0) {
return 0;
}
return form_head_width;
};
binding_list_config->should_prevent_inlining = [](FormFormattingConfig /*config*/, int num_refs) {
return false;
};
cfg.index_configs.emplace(1, binding_list_config);
return cfg;
}
static FormFormattingConfig new_pair_rule(bool combine_first_two_expr) {
FormFormattingConfig cfg;
cfg.config_set = true;
cfg.hang_forms = false;
cfg.prevent_inlining = true;
cfg.combine_first_two_lines = combine_first_two_expr;
auto pair_config = std::make_shared<FormFormattingConfig>();
pair_config->config_set = true;
pair_config->hang_forms = false;
pair_config->indentation_width = 1;
cfg.default_index_config = pair_config;
return cfg;
}
static FormFormattingConfig new_top_level_inline_form(bool elide_new_line) {
return {.force_inline = true, .elide_top_level_newline = elide_new_line};
}
const std::unordered_map<std::string, FormFormattingConfig> opengoal_form_config = {
{"case", new_pair_rule(true)},
{"case-str", new_pair_rule(true)},
{"cond", new_pair_rule(false)},
{"#cond", new_pair_rule(false)},
{"in-package", new_top_level_inline_form(true)},
{"bundles", new_top_level_inline_form(true)},
{"require", new_top_level_inline_form(true)},
{"def-art-elt", new_top_level_inline_form(true)},
{"def-joint-node", new_top_level_inline_form(true)},
{"declare-file", new_top_level_inline_form(false)},
{"defenum", new_defenum_rule()},
{"defmethod", new_defmethod_rule(3)},
{"lambda", new_lambda_rule(2)},
{"deftype", new_deftype_rule(3, 1, {3, 4, 5, 6})},
{"defun", new_flow_rule(3)},
{"defun-recursive", new_flow_rule(4)},
{"defun-debug-recursive", new_flow_rule(4)},
{"defun-debug", new_flow_rule(3)},
{"defbehavior", new_flow_rule(4)},
{"if", new_inlineable_flow_rule(2)},
{"#if", new_inlineable_flow_rule(2)},
{"define", new_permissive_flow_rule()},
{"def-mips2c", new_permissive_flow_rule()},
{"defconstant", new_permissive_flow_rule()},
{"defglobalconstant", new_permissive_flow_rule()},
{"defmethod-mips2c", new_permissive_flow_rule()},
{"define-extern", new_permissive_flow_rule()},
{"declare-type", new_permissive_flow_rule()},
{"defmacro", new_function_rule(3)},
{"desfun", new_function_rule(3)},
{"defskelgroup", new_flow_rule(2, true)},
{"defpartgroup", new_flow_rule(2, true)},
{"defpart", new_flow_rule(2, true)},
{"defstate", new_defstate_rule(3, true)},
{"behavior", new_flow_rule(2)},
{"dotimes", new_flow_rule(2)},
{"dolist", new_flow_rule(2)},
{"process-spawn-function", new_flow_rule(2)},
{"let", new_binding_rule(4)},
{"protect", new_binding_rule(4)},
{"let*", new_binding_rule(5)},
{"rlet", new_binding_rule(5)},
{"when", new_flow_rule(2)},
{"unless", new_flow_rule(2)},
{"with-profiler", new_flow_rule(2)},
{"with-pc", new_flow_rule(0)},
{"#unless", new_flow_rule(2)},
{"#when", new_flow_rule(2)},
{"countdown", new_flow_rule(2)},
{"until", new_flow_rule(2)},
{"loop", new_flow_rule(0)},
{"while", new_flow_rule(2)},
{"begin", new_flow_rule(0)},
{"seval", new_flow_rule(0)},
{"with-pp", new_flow_rule(0)},
{"with-gensyms", new_flow_rule(2)},
{"with-vf0", new_flow_rule(0)},
{"with-vf", new_inline_binding_rule(8)},
{"with-cnt-vif-block", new_inline_binding_rule(19)},
{"local-vars", new_inlinable_simple_flow_rule()},
{"with-dma-buffer-add-bucket", new_flow_rule(2)}};
} // namespace config
} // namespace formatter_rules
-47
View File
@@ -1,47 +0,0 @@
#pragma once
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
namespace formatter_rules {
namespace config {
struct FormFormattingConfig {
bool config_set = false;
bool force_inline = false;
bool hang_forms =
true; // TODO - remove this eventually, it's only involved in setting the
// indentation width, which we can do via the new indentation_width function
int indentation_width =
2; // 2 for a flow // TODO - also remove this, prefer storing the first node's width in the
// metadata on the first pass, that's basically all this does
std::function<int(FormFormattingConfig, int)> indentation_width_for_index =
[](FormFormattingConfig config, int /*index*/) { return config.indentation_width; };
bool combine_first_two_lines =
false; // NOTE - basically hang, but will probably stick around after hang is gone, may be
// redundant (inline_until_index!)
std::function<std::optional<int>(const std::vector<std::string>& /*curr_lines*/)>
inline_until_index = [](std::vector<std::string> /*curr_lines*/) { return std::nullopt; };
bool has_constant_pairs = false;
bool prevent_inlining = false; // TODO - duplicate of below
std::function<bool(FormFormattingConfig, int num_refs)> should_prevent_inlining =
[](FormFormattingConfig config, int /*num_refs*/) { return config.prevent_inlining; };
int parent_mutable_extra_indent = 0;
std::optional<std::shared_ptr<FormFormattingConfig>> default_index_config;
std::unordered_map<int, std::shared_ptr<FormFormattingConfig>> index_configs = {};
// TODO / NOTe - not fully implemented, only doing `parent_mutable_extra_indent` right now
std::unordered_map<int, std::shared_ptr<FormFormattingConfig>> index_config_override = {};
bool determine_column_widths_for_list_elements = false;
int num_columns_to_compute_widths = 0;
std::vector<int> list_element_column_widths = {};
bool elide_top_level_newline = false;
};
extern const std::unordered_map<std::string, FormFormattingConfig> opengoal_form_config;
} // namespace config
} // namespace formatter_rules
-221
View File
@@ -1,221 +0,0 @@
// clang-format off
#include "GlobalProfiler.h"
#include <chrono>
#include <cstring>
#include <thread>
#include "common/util/Assert.h"
#include "common/util/FileUtil.h"
#include "common/common_types.h"
#include "fmt/core.h"
#include "third-party/json.hpp"
#ifdef OS_POSIX
u64 get_current_tid() {
return (u64)pthread_self();
}
#else
#define NOMINMAX
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include "Processthreadsapi.h"
u64 get_current_tid() {
return (u64)GetCurrentThreadId();
}
#endif
#include "common/log/log.h"
#include "common/util/compress.h"
#include "common/util/string_util.h"
// clang-format on
u64 get_current_ts() {
return std::chrono::steady_clock::now().time_since_epoch().count();
}
GlobalProfiler::GlobalProfiler() {
m_t0 = get_current_ts();
m_nodes.resize(m_max_events);
}
void GlobalProfiler::update_event_buffer_size(size_t new_size) {
m_max_events = new_size;
m_nodes.resize(m_max_events);
}
void GlobalProfiler::set_waiting_for_event(const std::string& event_name) {
if (!event_name.empty()) {
m_waiting_for_event = event_name;
}
}
void GlobalProfiler::event(const char* name, ProfNode::Kind kind) {
if (m_waiting_for_event && m_waiting_for_event.value() == name) {
m_ignore_events = true;
}
if (!m_enabled || m_ignore_events) {
return;
}
size_t my_idx = (m_next_idx++ % m_nodes.size());
auto& node = m_nodes[my_idx];
node.ts = get_current_ts() - m_t0;
node.tid = get_current_tid();
node.kind = kind;
strncpy(node.name, name, sizeof(node.name));
node.name[sizeof(node.name) - 1] = '\0';
}
void GlobalProfiler::instant_event(const char* name) {
event(name, ProfNode::INSTANT);
}
void GlobalProfiler::root_event() {
instant_event("ROOT");
}
size_t GlobalProfiler::get_next_idx() {
return (m_next_idx % m_nodes.size());
}
void GlobalProfiler::begin_event(const char* name) {
event(name, ProfNode::BEGIN);
}
void GlobalProfiler::end_event() {
if (!m_enabled || m_ignore_events) {
return;
}
size_t my_idx = (m_next_idx++ % m_nodes.size());
auto& node = m_nodes[my_idx];
node.ts = get_current_ts() - m_t0;
node.tid = get_current_tid();
node.kind = ProfNode::END;
node.name[0] = '\0';
}
void GlobalProfiler::clear() {
m_next_idx = 0;
}
void GlobalProfiler::set_enable(bool en) {
m_enabled = en;
}
void GlobalProfiler::dump_to_json() {
if (m_enabled) {
set_enable(false);
}
nlohmann::json json;
auto& trace_events = json["traceEvents"];
json["displayTimeUnit"] = "ms";
u64 lowest_ts = UINT64_MAX;
struct ThreadInfo {
size_t lowest_at_target = UINT64_MAX;
size_t highest_at_target = 0;
size_t debug = 0;
u32 short_id = 0;
};
std::unordered_map<u32, ThreadInfo> info_per_thread;
// first, find all the threads
std::string kRootName = "ROOT";
for (size_t offset = m_nodes.size(); offset-- > 0;) {
size_t idx = (m_next_idx + offset) % m_nodes.size();
const auto& event = m_nodes[idx];
if (event.kind != ProfNode::UNUSED) {
lowest_ts = std::min(event.ts, lowest_ts);
}
if (event.kind == ProfNode::INSTANT && kRootName == event.name) {
auto& info = info_per_thread[event.tid];
info.lowest_at_target = std::min(info.lowest_at_target, offset);
info.highest_at_target = std::max(info.highest_at_target, offset);
}
}
u32 i = 0;
for (auto& info : info_per_thread) {
info.second.short_id = i++;
}
for (size_t event_idx = 0; event_idx < m_nodes.size(); event_idx++) {
auto& event = m_nodes[(event_idx + m_next_idx) % m_nodes.size()];
if (event.kind == ProfNode::UNUSED) {
continue;
}
auto& info = info_per_thread.at(event.tid);
if (event_idx < info.lowest_at_target || event_idx > info.highest_at_target) {
continue;
}
auto& json_event = trace_events.emplace_back();
// name
if (event.kind != ProfNode::END) {
json_event["name"] = event.name;
}
// cat
// json_event["cat"] = "a"; // ??
// ph BEi
switch (event.kind) {
case ProfNode::END:
json_event["ph"] = "E";
break;
case ProfNode::BEGIN:
json_event["ph"] = "B";
break;
case ProfNode::INSTANT:
json_event["ph"] = "i";
break;
default:
ASSERT(false);
}
// pid
json_event["pid"] = 1;
// tid
json_event["tid"] = info.short_id;
// ts
json_event["ts"] = (event.ts - lowest_ts) / 1000.f;
if (event.ts < info.debug) {
lg::debug("out of order: {} {} {} ms", event.ts / 1000.f, info.debug / 1000.f,
(info.debug - event.ts) / 1000000.f);
lg::debug(" idx: {}, range {} {}", event_idx, info.lowest_at_target, info.highest_at_target);
lg::debug(" now: {}", m_next_idx.load());
}
info.debug = event.ts;
}
for (auto& t : info_per_thread) {
lg::debug("thread: {}: {} -> {}", t.first, t.second.lowest_at_target,
t.second.highest_at_target);
}
if (m_enable_compression) {
const auto json_str = json.dump();
const auto compressed_data =
compression::compress_zstd_no_header(json_str.data(), json_str.size());
auto file_path = file_util::get_jak_project_dir() / "profile_data" /
fmt::format("prof-{}.json.zst", str_util::current_local_timestamp_no_colons());
file_util::create_dir_if_needed_for_file(file_path);
file_util::write_binary_file(file_path, compressed_data.data(), compressed_data.size());
} else {
auto file_path = file_util::get_jak_project_dir() / "profile_data" /
fmt::format("prof-{}.json", str_util::current_local_timestamp_no_colons());
file_util::create_dir_if_needed_for_file(file_path);
file_util::write_text_file(file_path, json.dump());
}
}
GlobalProfiler gprof;
GlobalProfiler& prof() {
return gprof;
}
ScopedEvent scoped_prof(const char* name) {
auto& p = prof();
p.begin_event(name);
return {&p};
}
-62
View File
@@ -1,62 +0,0 @@
#pragma once
#include <atomic>
#include <optional>
#include <string>
#include <vector>
#include "common/common_types.h"
struct ProfNode {
u64 ts;
u64 tid;
char name[128];
enum Kind : u8 { BEGIN, END, INSTANT, UNUSED } kind = UNUSED;
};
class GlobalProfiler {
public:
GlobalProfiler();
size_t get_max_events() { return m_max_events; }
void update_event_buffer_size(size_t new_size);
void set_waiting_for_event(const std::string& event_name);
void instant_event(const char* name);
void begin_event(const char* name);
void event(const char* name, ProfNode::Kind kind);
void end_event();
void clear();
void set_enable(bool en);
void dump_to_json();
void root_event();
bool is_enabled() { return m_enabled; }
size_t get_next_idx();
bool m_enable_compression = false;
private:
std::atomic_bool m_enabled = false;
size_t m_max_events = 65536;
u64 m_t0 = 0;
std::atomic_size_t m_next_idx = 0;
std::vector<ProfNode> m_nodes;
// this is very niche, but sometimes you want to capture up to a given event (ie. long startup)
// instead of having to make the user quit and record as fast as possible, we can instead just
// stop capturing events once we have received what we are looking for
std::optional<std::string> m_waiting_for_event = {};
bool m_ignore_events = false;
};
struct ScopedEvent {
ScopedEvent(GlobalProfiler* _prof) : prof(_prof){};
ScopedEvent(const ScopedEvent&) = delete;
ScopedEvent& operator=(const ScopedEvent&) = delete;
GlobalProfiler* prof = nullptr;
~ScopedEvent() {
if (prof) {
prof->end_event();
}
}
};
GlobalProfiler& prof();
ScopedEvent scoped_prof(const char* name);
-32
View File
@@ -1,32 +0,0 @@
# Event Profiler
The event profiler is a tool to analyze timing of multiple frames. Unlike sampling-based profilers, this profile captures an exact timeline of what happens of what happens when.
## Capturing a profile
In the OpenGOAL window, click "Profiler" and check "Record" to start recording. The buffer has a fixed maximum size and it will automatically overwrite old data once it is full.
When something interesting happens, click the "dump to file" button to save the buffer (currently a few seconds) to `prof.json` in `jak-project`.
The idea is that you can leave this running as you play, and then when the game stutters or does something interesting, you can click the dump button and get the result.
## Viewing a profile
Open Google Chrome and go to `chrome://tracing`. Then click load and open the json file. Or, just drag and drop the file into chrome.
Press `1` for a box drawing tool. This lets you select a region of the flame chart and get a list of events inside the box.
Press `2` for panning.
Press `3` for zooming.
## Adding an event
The GOAL kernel automatically adds events for each process. If you want to add another event, you can use `(with-profiler "name-of-event" <body>)`. Do not call `suspend` or do a `return` inside of this. If you need more control over stopping/starting, there are functions in `gcommon.gc` to explicitly start/stop events. But you must match them up correctly yourself!
In C++, the graphics profiler automatically adds a profiler nodes as events. To add an event, you can use
```auto p = scoped_prof("name-of-event");```
The event is active from this call until the destruction of `p`.
## Multiple threads
The event profiler currently works on both the graphics and EE threads. Adding the events can safely be done from any thread, but enable/disable/dump should be done from a single thread at a time.
Each thread should periodically insert a `ROOT` instant event when there are no active range events. This is required to make the retroactive dump feature work properly as the event buffer does not capture the tree structure fully, and it must be able to find a point in time when no events are active.
+5 -77
View File
@@ -1,7 +1,6 @@
#pragma once
#include "common/common_types.h"
#include "common/versions/versions.h"
#include "common_types.h"
constexpr s32 BINTEGER_OFFSET = 0;
constexpr s32 PAIR_OFFSET = 2;
@@ -10,79 +9,10 @@ constexpr int BASIC_OFFSET = 4;
constexpr int STRUCTURE_ALIGNMENT = 16;
constexpr int ARRAY_DATA_OFFSET = 12; // not including type tag
/*!
* Here you can change the size of the symbol table!
* Make sure to also edit the constant in gcommon.gc
*/
// constexpr s32 GOAL_MAX_SYMBOLS = 8192; // this MUST be a multiple of 2!!
// constexpr s32 SYM_INFO_OFFSET = 8167 * 8 - 4;
constexpr s32 GOAL_MAX_SYMBOLS = 0x2000;
constexpr s32 SYM_INFO_OFFSET = 0xff34;
namespace jak1 {
constexpr s32 ORIGINAL_MAX_GOAL_SYMBOLS = 8192;
constexpr s32 ORIGINAL_SYM_TO_STRING_OFFSET = 0xff38;
constexpr s32 GOAL_MAX_SYMBOLS = 16384;
constexpr s32 SYM_INFO_OFFSET = GOAL_MAX_SYMBOLS * 8 - 4;
constexpr s32 SYM_TABLE_MEM_SIZE = GOAL_MAX_SYMBOLS * 8 * 2;
constexpr int bits_for_sym() {
int b = -1;
for (int i = 0; i < 32; ++i) {
if ((GOAL_MAX_SYMBOLS & (1 << i)) != 0) {
if (b != -1) {
// already got a set bit... not a multiple of 2!
// throw 0;
return -1;
}
b = i;
}
}
return b + 1;
}
static_assert(bits_for_sym() != -1, "symbol table invalid length");
// amount of levels in level heap
constexpr int LEVEL_MAX = 2;
// total amount of levels, including ones outside level heap (default-level)
constexpr int LEVEL_TOTAL = LEVEL_MAX + 1;
} // namespace jak1
namespace jak2 {
// for now, we don't have the ability to extend the size of the symbol table
constexpr s32 GOAL_MAX_SYMBOLS = 0x4000;
constexpr s32 SYM_TABLE_MEM_SIZE = 0x30000;
// from the "off-by-one" symbol pointer
constexpr int SYM_TO_STRING_OFFSET = 0xff37;
constexpr int SYM_TO_HASH_OFFSET = 0x1fe6f;
// amount of levels in level heap
constexpr int LEVEL_MAX = 6;
// total amount of levels, including ones outside level heap (default-level)
constexpr int LEVEL_TOTAL = LEVEL_MAX + 1;
} // namespace jak2
namespace jak3 {
// for now, we don't have the ability to extend the size of the symbol table
constexpr s32 GOAL_MAX_SYMBOLS = 0x4000;
// amount of levels in level heap
constexpr int LEVEL_MAX = 10;
// total amount of levels, including ones outside level heap (default-level)
constexpr int LEVEL_TOTAL = LEVEL_MAX + 1;
} // namespace jak3
constexpr s32 max_symbols(GameVersion version) {
switch (version) {
case GameVersion::Jak1:
return jak1::GOAL_MAX_SYMBOLS;
case GameVersion::Jak2:
return jak2::GOAL_MAX_SYMBOLS;
case GameVersion::Jak3:
return jak3::GOAL_MAX_SYMBOLS;
}
}
enum class RegClass : u8 { GPR_64, FLOAT, INT_128, VECTOR_FLOAT, INVALID };
enum class RegClass { GPR_64, FLOAT, INT_128, VECTOR_FLOAT, INVALID };
constexpr u32 GOAL_NEW_METHOD = 0; // method ID of GOAL new
constexpr u32 GOAL_DEL_METHOD = 1; // method ID of GOAL delete
@@ -94,7 +24,7 @@ constexpr u32 GOAL_COPY_METHOD = 6; // method ID of GOAL copy
constexpr u32 GOAL_RELOC_METHOD = 7; // method ID of GOAL relocate
constexpr u32 GOAL_MEMUSAGE_METHOD = 8; // method ID of GOAL mem-usage
constexpr int EE_MAIN_MEM_LOW_PROTECT = 512 * 1024;
constexpr int EE_MAIN_MEM_LOW_PROTECT = 1024 * 1024;
constexpr int EE_MAIN_MEM_SIZE = 128 * (1 << 20); // 128 MB, same as PS2 TOOL
constexpr u64 EE_MAIN_MEM_MAP = 0x2123000000; // intentionally > 32-bit to catch pointer bugs
@@ -107,5 +37,3 @@ constexpr double METER_LENGTH = 4096.0;
constexpr double DEGREES_PER_ROT = 65536.0;
constexpr double DEGREES_LENGTH = DEGREES_PER_ROT / 360.0;
constexpr u64 TICKS_PER_SECOND = 300.0;
constexpr float DEFAULT_RES_TIME = -1000000000.0;
File diff suppressed because it is too large. Load diff
+24 -85
View File
@@ -7,32 +7,28 @@
#include <memory>
#include <optional>
#include "Object.h"
#include "Reader.h"
namespace goos {
class Interpreter {
public:
Interpreter(const std::string& user_profile = "#f");
Interpreter();
~Interpreter();
void execute_repl(REPL::Wrapper& repl);
void execute_repl(ReplWrapper& repl);
void throw_eval_error(const Object& o, const std::string& err);
Object eval_with_rewind(const Object& obj, const std::shared_ptr<EnvironmentObject>& env);
bool get_global_variable_by_name(const std::string& name, Object* dest);
void set_global_variable_by_name(const std::string& name, const Object& value);
void set_global_variable_to_symbol(const std::string& name, const std::string& value);
void set_global_variable_to_int(const std::string& name, int value);
Object eval(Object obj, const std::shared_ptr<EnvironmentObject>& env);
Object intern(const std::string& name);
InternedSymbolPtr intern_ptr(const std::string& name);
void disable_printfs();
Object eval_symbol(const Object& sym, const std::shared_ptr<EnvironmentObject>& env);
bool eval_symbol(const Object& sym,
const std::shared_ptr<EnvironmentObject>& env,
Object* result);
Arguments get_args(const Object& form, const Object& rest, const ArgumentSpec& spec);
Arguments get_args_no_named(const Object& form, const Object& rest, const ArgumentSpec& spec);
void set_args_in_env(const Object& form,
const Arguments& args,
const ArgumentSpec& arg_spec,
@@ -52,10 +48,17 @@ class Interpreter {
Object global_environment;
Object goal_env;
// data passed from GOAL to GOOS available to any evaluation.
struct GoalToGoosData {
std::string enclosing_method_type;
void reset() { enclosing_method_type = "#f"; }
} goal_to_goos;
private:
friend class Goal;
void load_goos_library();
void define_var_in_env(Object& env, const Object& var, const std::string& name);
void define_var_in_env(Object& env, Object& var, const std::string& name);
void expect_env(const Object& form, const Object& o);
void vararg_check(
const Object& form,
@@ -64,11 +67,8 @@ class Interpreter {
const std::unordered_map<std::string, std::pair<bool, std::optional<ObjectType>>>& named);
Object eval_pair(const Object& o, const std::shared_ptr<EnvironmentObject>& env);
public:
ArgumentSpec parse_arg_spec(const Object& form, Object& rest);
private:
Object quasiquote_helper(const Object& form, const std::shared_ptr<EnvironmentObject>& env);
IntType number_to_integer(const Object& obj);
@@ -121,18 +121,12 @@ class Interpreter {
Object eval_read(const Object& form,
Arguments& args,
const std::shared_ptr<EnvironmentObject>& env);
Object eval_read_data_file(const Object& form,
Arguments& args,
const std::shared_ptr<EnvironmentObject>& env);
Object eval_read_file(const Object& form,
Arguments& args,
const std::shared_ptr<EnvironmentObject>& env);
Object eval_load_file(const Object& form,
Arguments& args,
const std::shared_ptr<EnvironmentObject>& env);
Object eval_try_load_file(const Object& form,
Arguments& args,
const std::shared_ptr<EnvironmentObject>& env);
Object eval_print(const Object& form,
Arguments& args,
const std::shared_ptr<EnvironmentObject>& env);
@@ -190,6 +184,9 @@ class Interpreter {
Object eval_type(const Object& form,
Arguments& args,
const std::shared_ptr<EnvironmentObject>& env);
Object eval_current_method_type(const Object& form,
Arguments& args,
const std::shared_ptr<EnvironmentObject>& env);
Object eval_format(const Object& form,
Arguments& args,
const std::shared_ptr<EnvironmentObject>& env);
@@ -205,18 +202,6 @@ class Interpreter {
Object eval_string_append(const Object& form,
Arguments& args,
const std::shared_ptr<EnvironmentObject>& env);
Object eval_string_starts_with(const Object& form,
Arguments& args,
const std::shared_ptr<EnvironmentObject>& env);
Object eval_string_ends_with(const Object& form,
Arguments& args,
const std::shared_ptr<EnvironmentObject>& env);
Object eval_string_split(const Object& form,
Arguments& args,
const std::shared_ptr<EnvironmentObject>& env);
Object eval_string_substr(const Object& form,
Arguments& args,
const std::shared_ptr<EnvironmentObject>& env);
Object eval_ash(const Object& form,
Arguments& args,
const std::shared_ptr<EnvironmentObject>& env);
@@ -226,12 +211,6 @@ class Interpreter {
Object eval_string_to_symbol(const Object& form,
Arguments& args,
const std::shared_ptr<EnvironmentObject>& env);
Object eval_int_to_string(const Object& form,
Arguments& args,
const std::shared_ptr<EnvironmentObject>& env);
Object eval_get_env(const Object& form,
Arguments& args,
const std::shared_ptr<EnvironmentObject>& env);
// specials
Object eval_define(const Object& form,
@@ -264,67 +243,27 @@ class Interpreter {
Object eval_while(const Object& form,
const Object& rest,
const std::shared_ptr<EnvironmentObject>& env);
Object eval_let(const Object& form,
const Object& rest,
const std::shared_ptr<EnvironmentObject>& env);
Object eval_let_star(const Object& form,
const Object& rest,
const std::shared_ptr<EnvironmentObject>& env);
Object eval_let_common(const Object& form,
const Object& rest,
const std::shared_ptr<EnvironmentObject>& env,
bool star);
Object eval_make_string_hash_table(const Object& form,
Arguments& args,
const std::shared_ptr<EnvironmentObject>& env);
Object eval_hash_table_try_ref(const Object& form,
Arguments& args,
const std::shared_ptr<EnvironmentObject>& env);
Object eval_hash_table_set(const Object& form,
Arguments& args,
const std::shared_ptr<EnvironmentObject>& env);
const Object& true_or_false(bool val);
void init_special_forms(
const std::unordered_map<std::string,
Object (Interpreter::*)(const Object&,
const Object&,
const std::shared_ptr<EnvironmentObject>&)>&
forms);
void init_builtin_forms(
const std::unordered_map<std::string,
Object (Interpreter::*)(const Object&,
Arguments&,
const std::shared_ptr<EnvironmentObject>&)>&
forms);
bool want_exit = false;
bool disable_printing = false;
std::unordered_map<
void*,
Object (Interpreter::*)(const Object&, Arguments&, const std::shared_ptr<EnvironmentObject>&)>
std::unordered_map<std::string,
Object (Interpreter::*)(const Object& form,
Arguments& args,
const std::shared_ptr<EnvironmentObject>& env)>
builtin_forms;
std::vector<std::pair<
void*,
std::function<Object(const Object&, Arguments&, const std::shared_ptr<EnvironmentObject>&)>>>
std::unordered_map<
std::string,
std::function<Object(const Object&, Arguments&, const std::shared_ptr<EnvironmentObject>&)>>
m_custom_forms;
std::vector<std::pair<void*,
Object (Interpreter::*)(const Object& form,
const Object& rest,
const std::shared_ptr<EnvironmentObject>& env)>>
std::unordered_map<std::string,
Object (Interpreter::*)(const Object& form,
const Object& rest,
const std::shared_ptr<EnvironmentObject>& env)>
special_forms;
int64_t gensym_id = 0;
std::unordered_map<std::string, ObjectType> string_to_type;
const char* m_false_sym = nullptr;
const char* m_true_sym = nullptr;
Object m_false_object, m_true_object;
};
} // namespace goos
+24 -158
View File
@@ -38,93 +38,17 @@
*
*/
#include "Object.h"
#include <cinttypes>
#include <cstring>
#include "Object.h"
#include "common/util/FileUtil.h"
#include "common/util/crc32.h"
#include "third-party/fmt/core.h"
#include "common/util/print_float.h"
#include "fmt/core.h"
namespace goos {
SymbolTable::SymbolTable() {
m_power_of_two_size = 1; // 2 ^ 1 = 2
m_entries.resize(2);
m_used_entries = 0;
m_next_resize = (m_entries.size() * kMaxUsed);
m_mask = 0b1;
}
SymbolTable::~SymbolTable() {
for (auto& e : m_entries) {
delete[] e.name;
}
}
InternedSymbolPtr SymbolTable::intern(const char* str) {
size_t string_len = strlen(str);
u32 hash = crc32((const u8*)str, string_len);
// probe
for (u32 i = 0; i < m_entries.size(); i++) {
u32 slot_addr = (hash + i) & m_mask;
auto& slot = m_entries[slot_addr];
if (!slot.name) {
// not found, insert!
slot.hash = hash;
auto* name = new char[string_len + 1];
memcpy(name, str, string_len + 1);
slot.name = name;
m_used_entries++;
if (m_used_entries >= m_next_resize) {
resize();
return intern(str);
}
return {name};
} else {
if (slot.hash != hash) {
continue; // bad hash
}
if (strcmp(slot.name, str) != 0) {
continue; // bad name
}
return {slot.name};
}
}
// should be impossible to reach.
ASSERT_NOT_REACHED();
}
void SymbolTable::resize() {
m_power_of_two_size++;
m_mask = (1U << m_power_of_two_size) - 1;
std::vector<Entry> new_entries(m_entries.size() * 2);
for (const auto& old_entry : m_entries) {
if (old_entry.name) {
bool done = false;
for (u32 i = 0; i < new_entries.size(); i++) {
u32 slot_addr = (old_entry.hash + i) & m_mask;
auto& slot = new_entries[slot_addr];
if (!slot.name) {
slot.name = old_entry.name;
slot.hash = old_entry.hash;
done = true;
break;
}
}
ASSERT(done);
}
}
m_entries = std::move(new_entries);
m_next_resize = kMaxUsed * m_entries.size();
std::shared_ptr<EmptyListObject> gEmptyList = nullptr;
std::shared_ptr<EmptyListObject>& get_empty_list() {
return gEmptyList;
}
/*!
@@ -154,8 +78,6 @@ std::string object_type_to_string(ObjectType type) {
return "[macro]";
case ObjectType::ENVIRONMENT:
return "[environment]";
case ObjectType::STRING_HASH_TABLE:
return "[string-hash-table]";
default:
throw std::runtime_error("unknown object type in object_type_to_string");
}
@@ -167,7 +89,7 @@ std::string object_type_to_string(ObjectType type) {
template <>
std::string fixed_to_string(FloatType x) {
auto result = float_to_string(x);
ASSERT((float)x == (float)std::stod(result));
assert((float)x == (float)std::stod(result));
return result;
}
@@ -214,9 +136,14 @@ std::string fixed_to_string(char x) {
return {buff};
}
template <>
std::string fixed_to_string(InternedSymbolPtr x) {
return x.name_ptr;
/*!
* Create a new symbol object by interning
*/
Object SymbolObject::make_new(SymbolTable& st, const std::string& name) {
Object obj;
obj.type = ObjectType::SYMBOL;
obj.heap_obj = st.intern(name);
return obj;
}
/*!
@@ -224,73 +151,19 @@ std::string fixed_to_string(InternedSymbolPtr x) {
*/
Object build_list(const std::vector<Object>& objects) {
if (objects.empty()) {
return Object::make_empty_list();
return EmptyListObject::make_new();
}
// this is by far the most expensive part of parsing, so this is done a bit carefully.
// we maintain a std::shared_ptr<PairObject> that represents the list, built from back to front.
std::shared_ptr<PairObject> head =
std::make_shared<PairObject>(objects.back(), Object::make_empty_list());
Object empty = EmptyListObject::make_new();
Object head = PairObject::make_new(objects[0], empty);
Object last = head;
s64 idx = ((s64)objects.size()) - 2;
while (idx >= 0) {
Object next;
next.type = ObjectType::PAIR;
next.heap_obj = std::move(head);
head = std::make_shared<PairObject>();
head->car = objects[idx];
head->cdr = std::move(next);
idx--;
for (std::size_t i = 1; i < objects.size(); i++) {
last.as_pair()->cdr = PairObject::make_new(objects[i], empty);
last = last.as_pair()->cdr;
}
Object result;
result.type = ObjectType::PAIR;
result.heap_obj = head;
return result;
}
Object build_list(std::vector<Object>&& objects) {
if (objects.empty()) {
return Object::make_empty_list();
}
// this is by far the most expensive part of parsing, so this is done a bit carefully.
// we maintain a std::shared_ptr<PairObject> that represents the list, built from back to front.
std::shared_ptr<PairObject> head =
std::make_shared<PairObject>(objects.back(), Object::make_empty_list());
s64 idx = ((s64)objects.size()) - 2;
while (idx >= 0) {
Object next;
next.type = ObjectType::PAIR;
next.heap_obj = std::move(head);
head = std::make_shared<PairObject>();
head->car = std::move(objects[idx]);
head->cdr = std::move(next);
idx--;
}
Object result;
result.type = ObjectType::PAIR;
result.heap_obj = std::move(head);
return result;
}
/*!
* Is this a float object that's a power of two?
* NOTE: assumes 64-bit float.
*/
bool Object::is_power_of_2_float() const {
FloatType val = as_float();
u64 val_i = -1;
memcpy(&val_i, &val, sizeof(val));
u64 mantissa = val_i & ((1LL << 52) - 1);
u64 exponent = (val_i >> 52) & ((1LL << 11) - 1);
return mantissa == 0 && exponent != 0 && exponent != ((1LL << 11) - 1);
return head;
}
/*!
@@ -310,9 +183,8 @@ bool Object::operator==(const Object& other) const {
return float_obj == other.float_obj;
case ObjectType::CHAR:
return char_obj == other.char_obj;
case ObjectType::SYMBOL:
return symbol_obj == other.symbol_obj;
case ObjectType::SYMBOL:
case ObjectType::ENVIRONMENT:
case ObjectType::LAMBDA:
case ObjectType::MACRO:
@@ -335,8 +207,6 @@ bool Object::operator==(const Object& other) const {
}
return true;
}
case ObjectType::STRING_HASH_TABLE:
return as_string_hash_table()->data == other.as_string_hash_table()->data;
default:
throw std::runtime_error("equality not implemented for " + print());
@@ -344,11 +214,7 @@ bool Object::operator==(const Object& other) const {
}
bool Object::is_symbol(const std::string& name) const {
return is_symbol() && name == as_symbol().name_ptr;
}
bool Object::is_string(const std::string& val) const {
return is_string() && as_string()->data == val;
return is_symbol() && as_symbol()->name == name;
}
template <>
+130 -319
View File
@@ -38,27 +38,24 @@
* MACRO - a heap allocated object representing a GOOS macro
* ENVIRONMENT - a heap allocated object representing a GOOS environment
*
* STRING_HASH_TABLE - a heap allocated object representing a key-value table. Currently the keys
* can only be strings, like make-str-hash-table in MIT Scheme.
*
*/
#include <cstring>
#include <map>
#include <memory>
#include <stdexcept>
#include <string>
#include "common/util/assert.h"
#include <memory>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
#include <stdexcept>
#include <map>
#include "common/common_types.h"
#include "common/util/Assert.h"
#include "common/util/crc32.h"
namespace goos {
using FloatType = double;
using IntType = s64;
/*!
* All objects have one of these kinds
*/
@@ -70,45 +67,21 @@ enum class ObjectType : u8 {
INTEGER,
FLOAT,
CHAR,
SYMBOL,
// allocated
SYMBOL,
STRING,
PAIR,
ARRAY,
LAMBDA,
MACRO,
ENVIRONMENT,
STRING_HASH_TABLE,
INVALID
};
std::string object_type_to_string(ObjectType kind);
// Some objects are "fixed", meaning they are stored inline in the Object, and not heap allocated.
// These use value semantics and are copied on object assignment.
// For int/float, these are simply double/int64_t
using FloatType = double;
using IntType = s64;
// For symbols, we simply store a pointer to the symbol's name string.
// this is wrapped in a type just to avoid confusion with normal const char*.
struct InternedSymbolPtr {
const char* name_ptr;
struct hash {
auto operator()(const InternedSymbolPtr& x) const {
return std::hash<const void*>()((const void*)x.name_ptr);
}
};
bool operator==(const char* msg) const { return strcmp(msg, name_ptr) == 0; }
bool operator!=(const char* msg) const { return strcmp(msg, name_ptr) != 0; }
bool operator==(const std::string& str) const { return str == name_ptr; }
bool operator!=(const std::string& str) const { return str != name_ptr; }
bool operator==(const InternedSymbolPtr& other) const { return other.name_ptr == name_ptr; }
bool operator!=(const InternedSymbolPtr& other) const { return other.name_ptr != name_ptr; }
};
/*!
* By default, convert a fixed object data to string with std::to_string.
@@ -137,31 +110,6 @@ std::string fixed_to_string<char>(char);
template <>
std::string fixed_to_string<IntType>(IntType);
template <>
std::string fixed_to_string<InternedSymbolPtr>(InternedSymbolPtr);
class SymbolTable {
public:
SymbolTable(const SymbolTable&) = delete;
SymbolTable& operator=(const SymbolTable&) = delete;
SymbolTable();
~SymbolTable();
InternedSymbolPtr intern(const char* str);
private:
void resize();
int m_power_of_two_size = 0;
struct Entry {
u32 hash = 0;
const char* name = nullptr;
};
std::vector<Entry> m_entries;
int m_used_entries = 0;
int m_next_resize = 0;
u32 m_mask = 0;
static constexpr float kMaxUsed = 0.7;
};
/*!
* Common implementation for a fixed object
*/
@@ -186,8 +134,6 @@ class FixedObject {
return object_type_to_string(ObjectType::INTEGER);
if (std::is_same<T, char>())
return object_type_to_string(ObjectType::CHAR);
if (std::is_same<T, InternedSymbolPtr>())
return object_type_to_string(ObjectType::SYMBOL);
throw std::runtime_error("Unsupported FixedObject type");
}
};
@@ -198,7 +144,6 @@ class FixedObject {
using IntegerObject = FixedObject<int64_t>;
using FloatObject = FixedObject<double>;
using CharObject = FixedObject<char>;
using SymbolObject = FixedObject<InternedSymbolPtr>;
// Other objects are separate allocated on the heap. These objects should be HeapObjects.
@@ -212,24 +157,21 @@ class HeapObject {
// forward declare all HeapObjects
class PairObject;
class EnvironmentObject;
class SymbolObject;
class StringObject;
class LambdaObject;
class MacroObject;
class ArrayObject;
class StringHashTableObject;
// Wrapper Object class for all objects
class Object {
public:
std::shared_ptr<HeapObject> heap_obj = nullptr;
friend Object build_list(const std::vector<Object>& objects);
friend Object build_list(std::vector<Object>&& objects);
union {
IntegerObject integer_obj;
FloatObject float_obj;
CharObject char_obj;
SymbolObject symbol_obj;
};
ObjectType type = ObjectType::INVALID;
@@ -242,10 +184,6 @@ class Object {
return float_obj.print();
case ObjectType::CHAR:
return char_obj.print();
case ObjectType::SYMBOL:
return symbol_obj.print();
case ObjectType::EMPTY_LIST:
return "()";
default:
return heap_obj->print();
}
@@ -259,10 +197,6 @@ class Object {
return float_obj.inspect();
case ObjectType::CHAR:
return char_obj.inspect();
case ObjectType::SYMBOL:
return symbol_obj.inspect();
case ObjectType::EMPTY_LIST:
return "[empty list] ()\n";
default:
return heap_obj->inspect();
}
@@ -292,34 +226,59 @@ class Object {
return o;
}
static Object make_empty_list() {
Object o;
o.type = ObjectType::EMPTY_LIST;
return o;
std::shared_ptr<PairObject> as_pair() const {
if (type != ObjectType::PAIR) {
throw std::runtime_error("as_pair called on a " + object_type_to_string(type) + " " +
print());
}
return std::dynamic_pointer_cast<PairObject>(heap_obj);
}
static Object make_symbol(SymbolTable* table, const char* name) {
Object o;
o.type = ObjectType::SYMBOL;
o.symbol_obj.value = table->intern(name);
return o;
std::shared_ptr<EnvironmentObject> as_env() const {
if (type != ObjectType::ENVIRONMENT) {
throw std::runtime_error("as_env called on a " + object_type_to_string(type) + " " + print());
}
return std::dynamic_pointer_cast<EnvironmentObject>(heap_obj);
}
PairObject* as_pair() const;
EnvironmentObject* as_env() const;
std::shared_ptr<EnvironmentObject> as_env_ptr() const;
StringObject* as_string() const;
LambdaObject* as_lambda() const;
MacroObject* as_macro() const;
ArrayObject* as_array() const;
StringHashTableObject* as_string_hash_table() const;
const InternedSymbolPtr& as_symbol() const {
std::shared_ptr<SymbolObject> as_symbol() const {
if (type != ObjectType::SYMBOL) {
throw std::runtime_error("as_symbol called on a " + object_type_to_string(type) + " " +
print());
}
return symbol_obj.value;
return std::dynamic_pointer_cast<SymbolObject>(heap_obj);
}
std::shared_ptr<StringObject> as_string() const {
if (type != ObjectType::STRING) {
throw std::runtime_error("as_string called on a " + object_type_to_string(type) + " " +
print());
}
return std::dynamic_pointer_cast<StringObject>(heap_obj);
}
std::shared_ptr<LambdaObject> as_lambda() const {
if (type != ObjectType::LAMBDA) {
throw std::runtime_error("as_lambda called on a " + object_type_to_string(type) + " " +
print());
}
return std::dynamic_pointer_cast<LambdaObject>(heap_obj);
}
std::shared_ptr<MacroObject> as_macro() const {
if (type != ObjectType::MACRO) {
throw std::runtime_error("as_macro called on a " + object_type_to_string(type) + " " +
print());
}
return std::dynamic_pointer_cast<MacroObject>(heap_obj);
}
std::shared_ptr<ArrayObject> as_array() const {
if (type != ObjectType::ARRAY) {
throw std::runtime_error("as_array called on a " + object_type_to_string(type) + " " +
print());
}
return std::dynamic_pointer_cast<ArrayObject>(heap_obj);
}
IntType& as_int() {
@@ -363,26 +322,88 @@ class Object {
bool is_empty_list() const { return type == ObjectType::EMPTY_LIST; }
bool is_list() const { return type == ObjectType::EMPTY_LIST || type == ObjectType::PAIR; }
bool is_int() const { return type == ObjectType::INTEGER; }
bool is_int(int val) const { return type == ObjectType::INTEGER && val == as_int(); }
bool is_float() const { return type == ObjectType::FLOAT; }
bool is_float(float val) const { return type == ObjectType::FLOAT && val == as_float(); }
bool is_char() const { return type == ObjectType::CHAR; }
bool is_symbol() const { return type == ObjectType::SYMBOL; }
bool is_symbol(const std::string& name) const;
bool is_string() const { return type == ObjectType::STRING; }
bool is_string(const std::string& val) const;
bool is_pair() const { return type == ObjectType::PAIR; }
bool is_array() const { return type == ObjectType::ARRAY; }
bool is_env() const { return type == ObjectType::ENVIRONMENT; }
bool is_macro() const { return type == ObjectType::MACRO; }
bool is_string_hash_table() const { return type == ObjectType::STRING_HASH_TABLE; }
bool is_power_of_2_float() const;
bool operator==(const Object& other) const;
bool operator!=(const Object& other) const { return !((*this) == other); }
};
// There is a single heap allocated EmptyListObject.
class EmptyListObject;
std::shared_ptr<EmptyListObject>& get_empty_list();
class EmptyListObject : public HeapObject {
public:
EmptyListObject() = default;
static Object make_new() {
Object obj;
obj.type = ObjectType::EMPTY_LIST;
if (!get_empty_list()) {
get_empty_list() = std::make_shared<EmptyListObject>();
}
obj.heap_obj = get_empty_list();
return obj;
}
std::string print() const override { return "()"; }
std::string inspect() const override {
char buff[256];
sprintf(buff, "[empty list] ()\n");
return std::string(buff);
}
~EmptyListObject() = default;
};
class SymbolTable;
/*!
* A Symbol Object, which is just a wrapper around a string.
* The make_new function will correctly
*/
class SymbolObject : public HeapObject {
public:
std::string name;
explicit SymbolObject(std::string _name) : name(std::move(_name)) {}
static Object make_new(SymbolTable& st, const std::string& name);
std::string print() const override { return name; }
std::string inspect() const override { return "[symbol] " + name + "\n"; }
~SymbolObject() = default;
};
/*!
* A Symbol Table, which holds all symbols.
*/
class SymbolTable {
public:
std::shared_ptr<SymbolObject> intern(const std::string& name) {
auto kv = table.find(name);
if (kv == table.end()) {
auto iter = table.insert({name, std::make_shared<SymbolObject>(name)});
return (*iter.first).second;
} else {
return kv->second;
}
}
~SymbolTable() = default;
private:
std::unordered_map<std::string, std::shared_ptr<SymbolObject>> table;
};
class StringObject : public HeapObject {
public:
std::string data;
@@ -405,10 +426,9 @@ class PairObject : public HeapObject {
public:
Object car, cdr;
PairObject(const Object& car_, const Object& cdr_) : car(car_), cdr(cdr_) {}
PairObject() = default;
PairObject(Object car_, Object cdr_) : car(car_), cdr(cdr_) {}
static Object make_new(const Object& a, const Object& b) {
static Object make_new(Object a, Object b) {
Object obj;
obj.type = ObjectType::PAIR;
obj.heap_obj = std::make_shared<PairObject>(a, b);
@@ -457,131 +477,11 @@ class PairObject : public HeapObject {
~PairObject() = default;
};
template <typename T>
class InternedPtrMap {
public:
InternedPtrMap(const InternedPtrMap&) = delete;
InternedPtrMap& operator=(const InternedPtrMap&) = delete;
InternedPtrMap() { clear(); }
T* lookup(InternedSymbolPtr str) {
if (m_entries.size() < 10) {
for (auto& e : m_entries) {
if (e.key == str.name_ptr) {
return &e.value;
}
}
return nullptr;
}
u32 hash = crc32((const u8*)&str.name_ptr, sizeof(const char*));
// probe
for (u32 i = 0; i < m_entries.size(); i++) {
u32 slot_addr = (hash + i) & m_mask;
auto& slot = m_entries[slot_addr];
if (!slot.key) {
return nullptr;
} else {
if (slot.key != str.name_ptr) {
continue; // bad hash
}
return &slot.value;
}
}
// should be impossible to reach.
ASSERT_NOT_REACHED();
}
void set(InternedSymbolPtr ptr, const T& obj) {
u32 hash = crc32((const u8*)&ptr.name_ptr, sizeof(const char*));
// probe
for (u32 i = 0; i < m_entries.size(); i++) {
u32 slot_addr = (hash + i) & m_mask;
auto& slot = m_entries[slot_addr];
if (!slot.key) {
// not found, insert!
slot.key = ptr.name_ptr;
slot.value = obj;
m_used_entries++;
if (m_used_entries >= m_next_resize) {
resize();
}
return;
} else {
if (slot.key == ptr.name_ptr) {
slot.value = obj;
return;
}
}
}
// should be impossible to reach.
ASSERT_NOT_REACHED();
}
void clear() {
m_entries.clear();
m_power_of_two_size = 3; // 2 ^ 3 = 8
m_entries.resize(8);
m_used_entries = 0;
m_next_resize = (m_entries.size() * kMaxUsed);
m_mask = 0b111;
}
private:
struct Entry {
const char* key = nullptr;
T value;
};
std::vector<Entry> m_entries;
void resize() {
m_power_of_two_size++;
m_mask = (1U << m_power_of_two_size) - 1;
std::vector<Entry> new_entries(m_entries.size() * 2);
for (const auto& old_entry : m_entries) {
if (old_entry.key) {
bool done = false;
u32 hash = crc32((const u8*)&old_entry.key, sizeof(const char*));
for (u32 i = 0; i < new_entries.size(); i++) {
u32 slot_addr = (hash + i) & m_mask;
auto& slot = new_entries[slot_addr];
if (!slot.key) {
slot.key = old_entry.key;
slot.value = std::move(old_entry.value);
done = true;
break;
}
}
ASSERT(done);
}
}
m_entries = std::move(new_entries);
m_next_resize = kMaxUsed * m_entries.size();
}
int m_power_of_two_size = 0;
int m_used_entries = 0;
int m_next_resize = 0;
u32 m_mask = 0;
static constexpr float kMaxUsed = 0.7;
};
using EnvironmentMap = InternedPtrMap<Object>;
class EnvironmentObject : public HeapObject {
public:
std::string name;
std::shared_ptr<EnvironmentObject> parent_env;
// note: this is keyed on the address in the symbol table.
EnvironmentMap vars;
// this find work by any name string
Object* find(const char* n, SymbolTable* st) { return vars.lookup(st->intern(n)); }
Object* find(InternedSymbolPtr ptr) { return vars.lookup(ptr); }
std::unordered_map<std::shared_ptr<SymbolObject>, Object> vars;
EnvironmentObject() = default;
@@ -613,8 +513,11 @@ class EnvironmentObject : public HeapObject {
std::string inspect() const override {
std::string result = "[environment]\n name: " + name +
"\n parent: " + (parent_env ? parent_env->print() : "NONE") + "\n";
"\n parent: " + (parent_env ? parent_env->print() : "NONE") +
"\n vars:\n";
for (const auto& kv : vars) {
result += " " + kv.first->print() + ": " + kv.second.print() + "\n";
}
return result;
}
};
@@ -737,98 +640,6 @@ class ArrayObject : public HeapObject {
Object& operator[](size_t idx) { return data.at(idx); }
};
class StringHashTableObject : public HeapObject {
public:
std::unordered_map<std::string, Object> data;
StringHashTableObject() = default;
static Object make_new() {
Object obj;
obj.type = ObjectType::STRING_HASH_TABLE;
obj.heap_obj = std::make_shared<StringHashTableObject>();
return obj;
}
std::string print() const override {
std::string result = "{";
for (const auto& kv : data) {
result += '(';
result += kv.first;
result += ' ';
result += kv.second.print();
result += ')';
result += ' ';
}
if (!data.empty()) {
result.pop_back();
}
result += '}';
return result;
}
std::string inspect() const override {
return "[string-hash-table] kind: string, data: " + print() + "\n";
}
};
Object build_list(const std::vector<Object>& objects);
Object build_list(std::vector<Object>&& objects);
inline PairObject* Object::as_pair() const {
if (type != ObjectType::PAIR) {
throw std::runtime_error("as_pair called on a " + object_type_to_string(type) + " " + print());
}
return static_cast<PairObject*>(heap_obj.get());
}
inline EnvironmentObject* Object::as_env() const {
if (type != ObjectType::ENVIRONMENT) {
throw std::runtime_error("as_env called on a " + object_type_to_string(type) + " " + print());
}
return static_cast<EnvironmentObject*>(heap_obj.get());
}
inline std::shared_ptr<EnvironmentObject> Object::as_env_ptr() const {
if (type != ObjectType::ENVIRONMENT) {
throw std::runtime_error("as_env called on a " + object_type_to_string(type) + " " + print());
}
return std::dynamic_pointer_cast<EnvironmentObject>(heap_obj);
}
inline StringObject* Object::as_string() const {
if (type != ObjectType::STRING) {
throw std::runtime_error("as_string called on a " + object_type_to_string(type) + " " +
print());
}
return static_cast<StringObject*>(heap_obj.get());
}
inline LambdaObject* Object::as_lambda() const {
if (type != ObjectType::LAMBDA) {
throw std::runtime_error("as_lambda called on a " + object_type_to_string(type) + " " +
print());
}
return static_cast<LambdaObject*>(heap_obj.get());
}
inline MacroObject* Object::as_macro() const {
if (type != ObjectType::MACRO) {
throw std::runtime_error("as_macro called on a " + object_type_to_string(type) + " " + print());
}
return static_cast<MacroObject*>(heap_obj.get());
}
inline ArrayObject* Object::as_array() const {
if (type != ObjectType::ARRAY) {
throw std::runtime_error("as_array called on a " + object_type_to_string(type) + " " + print());
}
return static_cast<ArrayObject*>(heap_obj.get());
}
inline StringHashTableObject* Object::as_string_hash_table() const {
if (type != ObjectType::STRING_HASH_TABLE) {
throw std::runtime_error("as_string_hash_table called on a " + object_type_to_string(type) +
" " + print());
}
return static_cast<StringHashTableObject*>(heap_obj.get());
}
} // namespace goos
+9 -23
View File
@@ -1,7 +1,5 @@
#include "ParseHelpers.h"
#include "fmt/core.h"
namespace goos {
bool get_va(const goos::Object& rest, std::string* err_string, goos::Arguments* result) {
@@ -12,12 +10,12 @@ bool get_va(const goos::Object& rest, std::string* err_string, goos::Arguments*
auto arg = current.as_pair()->car;
// did we get a ":keyword"
if (arg.is_symbol() && arg.as_symbol().name_ptr[0] == ':') {
auto key_name = arg.as_symbol().name_ptr + 1;
if (arg.is_symbol() && arg.as_symbol()->name.at(0) == ':') {
auto key_name = arg.as_symbol()->name.substr(1);
// check for multiple definition of key
if (args.named.find(key_name) != args.named.end()) {
*err_string = fmt::format("Key argument {} multiply defined", key_name);
*err_string = "Key argument " + key_name + " multiply defined";
return false;
}
@@ -39,35 +37,23 @@ bool get_va(const goos::Object& rest, std::string* err_string, goos::Arguments*
return true;
}
void get_va_no_named(const goos::Object& rest, goos::Arguments* result) {
goos::Arguments args;
// loop over forms in list
goos::Object current = rest;
while (!current.is_empty_list()) {
args.unnamed.push_back(current.as_pair()->car);
current = current.as_pair()->cdr;
}
*result = args;
}
bool va_check(
const goos::Arguments& args,
const std::vector<std::optional<goos::ObjectType>>& unnamed,
const std::unordered_map<std::string, std::pair<bool, std::optional<goos::ObjectType>>>& named,
std::string* err_string) {
ASSERT(args.rest.empty());
assert(args.rest.empty());
if (unnamed.size() != args.unnamed.size()) {
*err_string = fmt::format("Got {} arguments, but expected {}",
std::to_string(args.unnamed.size()), std::to_string(unnamed.size()));
*err_string = "Got " + std::to_string(args.unnamed.size()) + " arguments, but expected " +
std::to_string(unnamed.size());
return false;
}
for (size_t i = 0; i < unnamed.size(); i++) {
if (unnamed[i].has_value() && unnamed[i] != args.unnamed[i].type) {
// special case -- an empty list is a valid pair
*err_string = fmt::format("Argument {} has type {} but {} was expected\nArgument is: {}", i,
object_type_to_string(args.unnamed[i].type),
object_type_to_string(unnamed[i].value()), args.unnamed[i].print());
*err_string = "Argument " + std::to_string(i) + " has type " +
object_type_to_string(args.unnamed[i].type) + " but " +
object_type_to_string(unnamed[i].value()) + " was expected";
return false;
}
}
+4 -6
View File
@@ -2,14 +2,12 @@
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
#include <unordered_map>
#include "Object.h"
namespace goos {
bool get_va(const goos::Object& rest, std::string* err_string, goos::Arguments* result);
void get_va_no_named(const goos::Object& rest, goos::Arguments* result);
bool va_check(
const goos::Arguments& args,
const std::vector<std::optional<goos::ObjectType>>& unnamed,
@@ -20,13 +18,13 @@ template <typename T>
void for_each_in_list(const goos::Object& list, T f) {
const goos::Object* iter = &list;
while (iter->is_pair()) {
const auto& lap = iter->as_pair();
auto lap = iter->as_pair();
f(lap->car);
iter = &lap->cdr;
}
ASSERT(iter->is_empty_list());
assert(iter->is_empty_list());
}
int list_length(const goos::Object& list);
} // namespace goos
} // namespace goos
+118 -37
View File
@@ -4,21 +4,40 @@
* It is not very good, but significantly better than putting everything on one line
*/
#include "PrettyPrinter.h"
#include <cstring>
#include "common/util/assert.h"
#include <stdexcept>
#include <utility>
#include <cstring>
#include "PrettyPrinter.h"
#include "Reader.h"
#include "common/goos/PrettyPrinter2.h"
#include "common/util/Assert.h"
#include "fmt/core.h"
#include "third-party/fmt/core.h"
namespace pretty_print {
namespace {
// the integer representation is used here instead, wouldn't want really long numbers
const std::unordered_set<u32> banned_floats = {};
// print these floats (shown as ints here) as a named constant instead
const std::unordered_map<u32, std::string> const_floats = {{0x40490fda, "PI"},
{0xc0490fda, "MINUS_PI"}};
} // namespace
/*!
* Print a float in a nice representation if possible, or an exact 32-bit integer constant to
* be reinterpreted.
*/
goos::Object float_representation(float value) {
u32 int_value;
memcpy(&int_value, &value, 4);
if (const_floats.find(int_value) != const_floats.end()) {
return pretty_print::to_symbol(const_floats.at(int_value));
} else if (banned_floats.find(int_value) == banned_floats.end()) {
return goos::Object::make_float(value);
} else {
return pretty_print::build_list("the-as", "float", fmt::format("#x{:x}", int_value));
}
}
/*!
* A single token which cannot be split between lines.
*/
@@ -98,7 +117,7 @@ void add_to_token_list(const goos::Object& obj, std::vector<FormToken>* tokens)
// look for a (quote x) to print as 'x
auto& first = obj.as_pair()->car;
if (first.is_symbol() && first.as_symbol() == "quote") {
if (first.is_symbol() && first.as_symbol()->name == "quote") {
auto& second = obj.as_pair()->cdr;
if (second.is_pair() && second.as_pair()->cdr.is_empty_list()) {
tokens->emplace_back(FormToken::TokenKind::QUOTE);
@@ -143,7 +162,7 @@ void add_to_token_list(const goos::Object& obj, std::vector<FormToken>* tokens)
case goos::ObjectType::ENVIRONMENT:
throw std::runtime_error("tried to pretty print a goos object kind which is not supported.");
default:
ASSERT(false);
assert(false);
}
}
@@ -250,19 +269,19 @@ void insertNewlineBefore(NodePool& pool, PrettyPrinterNode* node, int specialInd
* After this has been called, the entire selection should be reformatted with propagate_pretty
*/
void breakList(NodePool& pool, PrettyPrinterNode* leftParen) {
ASSERT(!leftParen->is_line_separator);
ASSERT(leftParen->tok->kind == FormToken::TokenKind::OPEN_PAREN);
assert(!leftParen->is_line_separator);
assert(leftParen->tok->kind == FormToken::TokenKind::OPEN_PAREN);
auto* rp = leftParen->paren;
ASSERT(rp->tok->kind == FormToken::TokenKind::CLOSE_PAREN);
assert(rp->tok->kind == FormToken::TokenKind::CLOSE_PAREN);
for (auto* n = leftParen->next; n && n != rp; n = n->next) {
if (!n->is_line_separator) {
if (n->tok->kind == FormToken::TokenKind::OPEN_PAREN) {
n = n->paren;
ASSERT(n->tok->kind == FormToken::TokenKind::CLOSE_PAREN);
assert(n->tok->kind == FormToken::TokenKind::CLOSE_PAREN);
insertNewlineAfter(pool, n, 0);
} else if (n->tok->kind != FormToken::TokenKind::WHITESPACE) {
ASSERT(n->tok->kind != FormToken::TokenKind::CLOSE_PAREN);
assert(n->tok->kind != FormToken::TokenKind::CLOSE_PAREN);
insertNewlineAfter(pool, n, 0);
}
}
@@ -340,7 +359,7 @@ static PrettyPrinterNode* propagatePretty(NodePool& pool,
* Get the token on the start of the next line. nullptr if we're the last line.
*/
PrettyPrinterNode* getNextLine(PrettyPrinterNode* start) {
ASSERT(!start->is_line_separator);
assert(!start->is_line_separator);
int line = start->line;
for (;;) {
if (start->is_line_separator || start->line == line) {
@@ -361,7 +380,7 @@ PrettyPrinterNode* getNextLine(PrettyPrinterNode* start) {
*/
PrettyPrinterNode* getNextListOnLine(PrettyPrinterNode* start) {
int line = start->line;
ASSERT(!start->is_line_separator);
assert(!start->is_line_separator);
if (!start->next || start->next->is_line_separator) {
return nullptr;
}
@@ -382,7 +401,7 @@ PrettyPrinterNode* getNextListOnLine(PrettyPrinterNode* start) {
PrettyPrinterNode* getNextOfKindOrStringOnLine(PrettyPrinterNode* start,
FormToken::TokenKind kind) {
int line = start->line;
ASSERT(!start->is_line_separator);
assert(!start->is_line_separator);
if (!start->next || start->next->is_line_separator) {
return nullptr;
}
@@ -402,7 +421,7 @@ PrettyPrinterNode* getNextOfKindOrStringOnLine(PrettyPrinterNode* start,
PrettyPrinterNode* getNextListOrEmptyListOnLine(PrettyPrinterNode* start) {
int line = start->line;
ASSERT(!start->is_line_separator);
assert(!start->is_line_separator);
if (!start->next || start->next->is_line_separator) {
return nullptr;
}
@@ -456,7 +475,7 @@ loop_end:
*/
PrettyPrinterNode* getFirstListOnLine(PrettyPrinterNode* start) {
int line = start->line;
ASSERT(!start->is_line_separator);
assert(!start->is_line_separator);
while (!start->is_line_separator && start->line == line) {
if (start->tok->kind == FormToken::TokenKind::OPEN_PAREN)
return start;
@@ -471,12 +490,12 @@ PrettyPrinterNode* getFirstListOnLine(PrettyPrinterNode* start) {
* Get the first token on the first line which exceeds the max length
*/
PrettyPrinterNode* getFirstBadLine(PrettyPrinterNode* start, int line_length) {
ASSERT(!start->is_line_separator);
assert(!start->is_line_separator);
int currentLine = start->line;
auto* currentLineNode = start;
for (;;) {
if (start->is_line_separator) {
ASSERT(start->next);
assert(start->next);
start = start->next;
} else {
if (start->line != currentLine) {
@@ -520,7 +539,7 @@ void insertBreaksAsNeeded(NodePool& pool, PrettyPrinterNode* head, int line_leng
break;
// okay, we have a line which needs fixing.
ASSERT(!candidate_line->prev || candidate_line->prev->is_line_separator);
assert(!candidate_line->prev || candidate_line->prev->is_line_separator);
PrettyPrinterNode* form_to_start = getFirstListOnLine(candidate_line);
for (;;) {
if (!form_to_start) {
@@ -549,10 +568,10 @@ void insertBreaksAsNeeded(NodePool& pool, PrettyPrinterNode* head, int line_leng
* After this has been called, the entire selection should be reformatted with propagate_pretty
*/
void breakList(NodePool& pool, PrettyPrinterNode* leftParen, PrettyPrinterNode* first_elt) {
ASSERT(!leftParen->is_line_separator);
ASSERT(leftParen->tok->kind == FormToken::TokenKind::OPEN_PAREN);
assert(!leftParen->is_line_separator);
assert(leftParen->tok->kind == FormToken::TokenKind::OPEN_PAREN);
auto* rp = leftParen->paren;
ASSERT(rp->tok->kind == FormToken::TokenKind::CLOSE_PAREN);
assert(rp->tok->kind == FormToken::TokenKind::CLOSE_PAREN);
bool breaking = false;
for (auto* n = leftParen->next; n && n != rp; n = n->next) {
@@ -562,13 +581,13 @@ void breakList(NodePool& pool, PrettyPrinterNode* leftParen, PrettyPrinterNode*
if (!n->is_line_separator) {
if (n->tok->kind == FormToken::TokenKind::OPEN_PAREN) {
n = n->paren;
ASSERT(n->tok->kind == FormToken::TokenKind::CLOSE_PAREN);
assert(n->tok->kind == FormToken::TokenKind::CLOSE_PAREN);
if (breaking) {
insertNewlineAfter(pool, n, 0);
}
} else if (n->tok->kind != FormToken::TokenKind::WHITESPACE) {
ASSERT(n->tok->kind != FormToken::TokenKind::CLOSE_PAREN);
assert(n->tok->kind != FormToken::TokenKind::CLOSE_PAREN);
if (breaking) {
insertNewlineAfter(pool, n, 0);
}
@@ -607,8 +626,7 @@ void insertSpecialBreaks(NodePool& pool, PrettyPrinterNode* node) {
}
if (name == "defun" || name == "defmethod" || name == "defun-debug" || name == "let" ||
name == "let*" || name == "rlet" || name == "defbehavior" || name == "lambda" ||
name == "defstate" || name == "behavior") {
name == "let*" || name == "rlet" || name == "defbehavior" || name == "lambda") {
auto* first_list = getNextListOrEmptyListOnLine(node);
if (first_list) {
if (first_list->tok->kind == FormToken::TokenKind::EMPTY_PAIR) {
@@ -696,9 +714,9 @@ void insertSpecialBreaks(NodePool& pool, PrettyPrinterNode* node) {
while (true) {
// let's break this case:
ASSERT(start_of_case->tok->kind == FormToken::TokenKind::OPEN_PAREN);
assert(start_of_case->tok->kind == FormToken::TokenKind::OPEN_PAREN);
auto end_of_case = start_of_case->paren;
ASSERT(end_of_case->tok->kind == FormToken::TokenKind::CLOSE_PAREN);
assert(end_of_case->tok->kind == FormToken::TokenKind::CLOSE_PAREN);
// get the first thing in the case
// and break there.
@@ -744,11 +762,11 @@ void insertSpecialBreaks(NodePool& pool, PrettyPrinterNode* node) {
}
}
std::string to_string_v1(const goos::Object& obj, int line_length) {
std::string to_string(const goos::Object& obj, int line_length) {
NodePool pool;
std::vector<FormToken> tokens;
add_to_token_list(obj, &tokens);
ASSERT(!tokens.empty());
assert(!tokens.empty());
std::string pretty;
// build linked list of nodes
@@ -782,8 +800,8 @@ std::string to_string_v1(const goos::Object& obj, int line_length) {
n->paren = parenStack.back();
}
}
ASSERT(parenStack.size() == 1);
ASSERT(!parenStack.back());
assert(parenStack.size() == 1);
assert(!parenStack.back());
insertSpecialBreaks(pool, head);
propagatePretty(pool, head, line_length);
@@ -809,4 +827,67 @@ std::string to_string_v1(const goos::Object& obj, int line_length) {
return pretty;
}
goos::Reader pretty_printer_reader;
goos::Reader& get_pretty_printer_reader() {
return pretty_printer_reader;
}
goos::Object to_symbol(const std::string& str) {
return goos::SymbolObject::make_new(pretty_printer_reader.symbolTable, str);
}
goos::Object build_list(const std::string& str) {
return build_list(to_symbol(str));
}
goos::Object build_list(const goos::Object& obj) {
return goos::PairObject::make_new(obj, goos::EmptyListObject::make_new());
}
goos::Object build_list(const std::vector<goos::Object>& objects) {
if (objects.empty()) {
return goos::EmptyListObject::make_new();
} else {
return build_list(objects.data(), objects.size());
}
}
// build a list out of an array of forms
goos::Object build_list(const goos::Object* objects, int count) {
assert(count);
auto car = objects[0];
goos::Object cdr;
if (count - 1) {
cdr = build_list(objects + 1, count - 1);
} else {
cdr = goos::EmptyListObject::make_new();
}
return goos::PairObject::make_new(car, cdr);
}
// build a list out of a vector of strings that are converted to symbols
goos::Object build_list(const std::vector<std::string>& symbols) {
if (symbols.empty()) {
return goos::EmptyListObject::make_new();
}
std::vector<goos::Object> f;
f.reserve(symbols.size());
for (auto& x : symbols) {
f.push_back(to_symbol(x));
}
return build_list(f.data(), f.size());
}
void append(goos::Object& _in, const goos::Object& add) {
auto* in = &_in;
while (in->is_pair() && !in->as_pair()->cdr.is_empty_list()) {
in = &in->as_pair()->cdr;
}
if (!in->is_pair()) {
assert(false); // invalid list
}
in->as_pair()->cdr = add;
}
} // namespace pretty_print
+41 -4
View File
@@ -8,14 +8,51 @@
#include <string>
#include <vector>
#include "common/goos/Object.h"
#include "common/goos/PrettyPrinter2.h"
#include "common/goos/Printer.h"
#include "common/goos/Reader.h"
namespace pretty_print {
// main pretty print function
std::string to_string_v1(const goos::Object& obj, int line_length = 80);
std::string to_string(const goos::Object& obj, int line_length = 80);
// string -> object (as a symbol)
goos::Object to_symbol(const std::string& str);
// list with a single symbol from a string
goos::Object build_list(const std::string& str);
// wrap an object in a list
goos::Object build_list(const goos::Object& obj);
// build a list out of a vector of forms
goos::Object build_list(const std::vector<goos::Object>& objects);
// build a list out of an array of forms
goos::Object build_list(const goos::Object* objects, int count);
// build a list out of a vector of strings that are converted to symbols
goos::Object build_list(const std::vector<std::string>& symbols);
// fancy wrapper functions. Due to template magic these can call each other
// and accept mixed arguments!
template <typename... Args>
goos::Object build_list(const goos::Object& car, Args... rest);
template <typename... Args>
goos::Object build_list(const std::string& str, Args... rest) {
return goos::PairObject::make_new(to_symbol(str), build_list(rest...));
}
template <typename... Args>
goos::Object build_list(const goos::Object& car, Args... rest) {
return goos::PairObject::make_new(car, build_list(rest...));
}
goos::Reader& get_pretty_printer_reader();
goos::Object float_representation(float value);
void append(goos::Object& _in, const goos::Object& add);
} // namespace pretty_print
-541
View File
@@ -1,541 +0,0 @@
#include "PrettyPrinter2.h"
#include "common/common_types.h"
#include "common/util/Assert.h"
#include "fmt/core.h"
#include "fmt/format.h"
namespace pretty_print {
namespace v2 {
// Note: there's some recursive stuff, but we only recurse once per list depth.
// The previous issues we had with stack overflow only happened when there was a stack frame per
// element in a list.
// The main node type.
// unlike v1, this nests lists.
// these have pointers to parents, so generally not safe to copy.
struct Node {
Node() = default;
Node(const std::string& str) : kind(Kind::ATOM), atom_str(str) {}
Node(std::vector<Node>&& list, bool is_list)
: kind(is_list ? Kind::LIST : Kind::IMPROPER_LIST), child_nodes(std::move(list)) {}
enum class Kind : u8 { ATOM, LIST, IMPROPER_LIST, INVALID } kind = Kind::INVALID;
std::vector<Node> child_nodes;
std::string atom_str;
// number of quotes this is wrapped in.
enum class QuoteKind { QUOTE, UNQUOTE, QUASIQUOTE, UNQUOTE_SPLICING };
std::vector<QuoteKind> quotes;
Node* parent = nullptr;
u32 my_depth = 0;
int get_quote_length() const;
void link(Node* this_parent, std::vector<Node*>* bfs_order, u32 depth) {
parent = this_parent;
my_depth = depth;
bfs_order->push_back(this);
switch (kind) {
case Kind::ATOM:
break;
case Kind::LIST:
case Kind::IMPROPER_LIST:
ASSERT(!child_nodes.empty());
for (auto& child : child_nodes) {
child.link(this, bfs_order, depth + 1);
}
break;
default:
ASSERT(false);
}
}
bool needs_end_paren_newline() const {
if (break_list) {
return true;
}
if (!child_nodes.empty()) {
return child_nodes.back().needs_end_paren_newline();
}
return false;
}
std::string debug_to_string() const {
switch (kind) {
case Kind::ATOM:
return fmt::format("[atom {}]", atom_str);
case Kind::LIST:
return "[list]";
case Kind::IMPROPER_LIST:
return "[improper list]";
default:
ASSERT(false);
}
}
// how wide is this text? not including the indentation of this subtree.
u32 text_len = 0;
bool break_list = false;
u8 top_line_count = 0;
u8 sub_elt_indent = 0;
};
inline const std::string quote_symbol(Node::QuoteKind kind) {
switch (kind) {
case Node::QuoteKind::QUOTE:
return "'";
case Node::QuoteKind::QUASIQUOTE:
return "`";
case Node::QuoteKind::UNQUOTE:
return ",";
case Node::QuoteKind::UNQUOTE_SPLICING:
return ",@";
default:
ASSERT_MSG(false, fmt::format("invalid quote kind {}", fmt::underlying(kind)));
return "[invalid]";
}
}
int Node::get_quote_length() const {
int out = 0;
for (auto& q : quotes) {
out += quote_symbol(q).length();
}
return out;
}
Node to_node(const goos::Object& obj) {
switch (obj.type) {
case goos::ObjectType::EMPTY_LIST:
// just treat this as a printing "atom"
return Node("()");
case goos::ObjectType::INTEGER:
case goos::ObjectType::FLOAT:
case goos::ObjectType::CHAR:
case goos::ObjectType::SYMBOL:
case goos::ObjectType::STRING:
// these are all atoms that the pretty printer should just treat as a blob.
return Node(obj.print());
case goos::ObjectType::PAIR: {
// we've got four cases: quoted thing, unquoted thing, proper list, improper list.
// there's probably a better way to do this but i am lazy
auto& first = obj.as_pair()->car;
if (first.is_symbol("quote")) {
auto& second = obj.as_pair()->cdr;
if (second.is_pair() && second.as_pair()->cdr.is_empty_list()) {
Node result = to_node(second.as_pair()->car);
result.quotes.push_back(Node::QuoteKind::QUOTE);
return result;
}
} else if (first.is_symbol("unquote")) {
auto& second = obj.as_pair()->cdr;
if (second.is_pair() && second.as_pair()->cdr.is_empty_list()) {
Node result = to_node(second.as_pair()->car);
result.quotes.push_back(Node::QuoteKind::UNQUOTE);
return result;
}
}
// not quoted, so either list or pair
std::vector<Node> children;
auto* to_print = &obj;
for (;;) {
if (to_print->is_pair()) {
// first print the car:
children.push_back(to_node(to_print->as_pair()->car));
// then load up the cdr as the next thing to print
to_print = &to_print->as_pair()->cdr;
if (to_print->is_empty_list()) {
// we're done, add a close paren and finish
return Node(std::move(children), true);
}
} else {
children.push_back(to_node(*to_print));
return Node(std::move(children), false);
}
}
} break;
// these are unsupported by the pretty printer.
case goos::ObjectType::ARRAY: // todo, we should probably handle arrays.
case goos::ObjectType::LAMBDA:
case goos::ObjectType::MACRO:
case goos::ObjectType::ENVIRONMENT:
throw std::runtime_error("tried to pretty print a goos object kind which is not supported.");
default:
ASSERT(false);
}
}
void recompute_lengths(const std::vector<Node*>& bfs_order) {
// iterate from leaves up
for (auto it = bfs_order.rbegin(); it != bfs_order.rend(); it++) {
Node* node = *it;
switch (node->kind) {
case Node::Kind::ATOM:
node->text_len = node->atom_str.length() + node->get_quote_length();
break;
case Node::Kind::IMPROPER_LIST:
case Node::Kind::LIST: {
if (node->break_list) {
// special case compute first line length
int first_line_len = 1 + node->get_quote_length(); // open paren + quotes
int nodes_on_first_line =
std::min(int(node->child_nodes.size()), int(node->top_line_count));
if (nodes_on_first_line > 0) {
for (int node_idx = 0; node_idx < nodes_on_first_line; node_idx++) {
first_line_len += node->child_nodes.at(node_idx).text_len;
first_line_len++; // trailing space
}
first_line_len--; // last one doesn't have a trailing space
}
int max_line_len = first_line_len;
// now the length of all the things below
for (u32 node_idx = nodes_on_first_line; node_idx < node->child_nodes.size();
node_idx++) {
int line_len = node->sub_elt_indent + node->child_nodes.at(node_idx).text_len;
max_line_len = std::max(max_line_len, line_len);
}
node->text_len = max_line_len;
} else {
node->text_len = 1 + node->get_quote_length(); // open paren + quotes
for (auto& child : node->child_nodes) {
node->text_len += (child.text_len + 1); // space or close paren.
}
}
} break;
default:
ASSERT(false);
}
}
}
/*!
* Note: this has special cases for how to insert breaks.
* These rules will be used if the printer decides it should break up the list.
* If you want to force a form to always be broken up, see insert_required_breaks
*/
void break_list(Node* node) {
ASSERT(!node->break_list);
node->break_list = true;
node->sub_elt_indent = 2;
node->top_line_count = 1;
const std::unordered_set<std::string> sameline_splitters = {
"if",
"<",
">",
"<=",
">=",
"set!",
"=",
"!=",
"+",
"-",
"*",
"/",
"the",
"->",
"and",
"or",
"logand",
"logior",
"logxor",
"+!",
"*!",
"logtest?",
"not",
"zero?",
"nonzero?",
"dma-buffer-add-gs-set",
"dma-buffer-add-gs-set-flusha",
};
if (node->child_nodes.at(0).kind == Node::Kind::LIST) {
// ((foo
// bar
node->sub_elt_indent = 1;
} else if (node->child_nodes.at(0).kind == Node::Kind::ATOM) {
auto& name = node->child_nodes[0].atom_str;
if (name == "defun" || name == "defun-debug" || name == "defbehavior" || name == "defstate") {
// things with three things in the top line: (defun <name> <args>
node->top_line_count = 3;
} else if (name == "defskelgroup") {
// things with 5 things in the top line: (defskelgroup <name> <art> jgeo janim
node->top_line_count = 5;
node->sub_elt_indent += name.size();
} else if (name == "process-new") {
// things with 3 things in the top line
node->top_line_count = 3;
node->sub_elt_indent += name.size();
} else if (name == "ja" || name == "ja-no-eval") {
node->top_line_count = 3;
node->sub_elt_indent += name.size();
} else if (name == "defmethod") {
// things with 4 things in the top line: (defmethod <method> <type> <args>
// or just 3 things in the top line: (defmethod <method> <args>
node->top_line_count = 3;
if (node->child_nodes.size() >= 4 && node->child_nodes[2].kind == Node::Kind::ATOM) {
node->top_line_count = 4;
}
} else if (name == "until" || name == "while" || name == "dotimes" || name == "countdown" ||
name == "when" || name == "behavior" || name == "lambda" || name == "defpart" ||
name == "define") {
node->top_line_count = 2;
} else if (name == "let" || name == "let*" || name == "rlet" ||
name == "with-dma-buffer-add-bucket") {
// special case for things like let.
node->top_line_count = 2; // (let <defs>
if (node->child_nodes.size() > 1 && node->child_nodes[1].child_nodes.size() > 1 &&
!node->child_nodes[1].break_list) {
// and break the defs.
break_list(&node->child_nodes[1]);
}
} else if (sameline_splitters.count(name) > 0) {
// if has a special indent rule:
node->top_line_count = 2;
node->sub_elt_indent += name.size();
} else if (name == "cond") {
// cond should always be broken up
for (size_t i = 1; i < node->child_nodes.size(); i++) {
auto& cond_body = node->child_nodes[i];
if (cond_body.kind == Node::Kind::LIST && !cond_body.break_list) {
break_list(&cond_body);
}
}
} else if (name == "case") {
// case gets a second thing on top, plus break up everything.
node->top_line_count = 2;
for (size_t i = 2; i < node->child_nodes.size(); i++) {
auto& cond_body = node->child_nodes[i];
if (cond_body.kind == Node::Kind::LIST && !cond_body.break_list) {
break_list(&cond_body);
}
}
}
}
Node* child = node;
for (Node* p = node->parent; p; p = p->parent) {
if (!p->break_list && &p->child_nodes.back() != child) {
break_list(p);
}
child = p;
}
}
void insert_required_breaks(const std::vector<Node*>& bfs_order) {
const std::unordered_set<std::string> always_break = {
"when", "defun-debug", "countdown", "case", "defun", "defmethod", "let",
"until", "while", "if", "dotimes", "cond", "else", "defbehavior",
"with-pp", "rlet", "defstate", "behavior", "defpart", "loop", "let*"};
for (auto node : bfs_order) {
if (!node->break_list && node->kind == Node::Kind::LIST &&
node->child_nodes.at(0).kind == Node::Kind::ATOM) {
if (always_break.count(node->child_nodes[0].atom_str) > 0) {
break_list(node);
}
}
}
}
int run_algorithm(const std::vector<Node*>& bfs_order, int line_length) {
// our approach is to go in reverse order and find the first list node that is:
// - too long
// - not already split.
// the "magic" of v2 is:
// the "too long" check above ignores the sublist.
int num_broken = 0;
std::optional<s32> min_depth;
for (auto it = bfs_order.rbegin(); it != bfs_order.rend(); it++) {
Node* node = *it;
if (min_depth && node->my_depth < min_depth) {
break;
}
if (node->kind != Node::Kind::ATOM && (int)node->text_len > line_length &&
node->break_list == false) {
break_list(node);
num_broken++;
if (!min_depth) {
min_depth = node->my_depth;
}
}
}
recompute_lengths(bfs_order);
return num_broken;
}
int compute_extra_offset(const std::string& str, int s0, int ei) {
ASSERT(!str.empty());
for (size_t i = str.length(); i-- > 0;) {
if ((int)i == s0) {
return ei + str.length() - s0;
} else if (i == '\n') {
return str.length() - i;
}
}
return ei + str.length() - s0;
}
void append_node_to_string(const Node* node,
std::string& str,
int init_indent_level,
int next_indent_level) {
for (int i = 0; i < init_indent_level; i++) {
str.push_back(' ');
}
for (auto q : node->quotes) {
str.append(quote_symbol(q));
}
switch (node->kind) {
case Node::Kind::ATOM:
str.append(node->atom_str);
break;
case Node::Kind::IMPROPER_LIST:
case Node::Kind::LIST:
if (node->break_list) {
str.push_back('(');
size_t node_idx = 0;
int listing_indent = next_indent_level + node->get_quote_length() + node->sub_elt_indent;
int extra_indent = 0;
int old_indent = listing_indent;
if (node->top_line_count) {
listing_indent -= node->sub_elt_indent;
listing_indent += node->child_nodes.front().kind == Node::Kind::LIST ? 1 : 2;
}
for (; node_idx < node->top_line_count; node_idx++) {
size_t s0 = str.length();
if (node->kind == Node::Kind::IMPROPER_LIST &&
&node->child_nodes.at(node_idx) == &node->child_nodes.back()) {
str.append(". ");
}
// so, if these need to break, they should have a bigger indent.
append_node_to_string(&node->child_nodes.at(node_idx), str, 0,
listing_indent + extra_indent);
extra_indent = compute_extra_offset(str, s0, extra_indent);
str.push_back(' ');
}
if (node->top_line_count) {
listing_indent = old_indent;
}
if (node->top_line_count > 0) {
str.pop_back();
}
str.push_back('\n');
bool after_key = false;
for (; node_idx < node->child_nodes.size(); node_idx++) {
if (node->kind == Node::Kind::IMPROPER_LIST &&
&node->child_nodes.at(node_idx) == &node->child_nodes.back()) {
for (int i = 0; i < listing_indent; i++) {
str.push_back(' ');
}
str.append(".\n");
}
append_node_to_string(&node->child_nodes.at(node_idx), str,
after_key ? 0 : listing_indent, listing_indent);
if (node->child_nodes.at(node_idx).kind == Node::Kind::ATOM &&
node->child_nodes.at(node_idx).atom_str.at(0) == ':' &&
node->child_nodes.at(node_idx).atom_str.find(' ') == std::string::npos) {
str.push_back(' ');
after_key = true;
} else {
str.push_back('\n');
after_key = false;
}
}
for (int i = 0; i < listing_indent; i++) {
str.push_back(' ');
}
str.push_back(')');
} else {
str.push_back('(');
ASSERT(!node->child_nodes.empty());
int listing_indent = next_indent_level + node->get_quote_length();
int extra_indent = 1;
int c0 = 0;
for (auto& child : node->child_nodes) {
if (node->kind == Node::Kind::IMPROPER_LIST && &child == &node->child_nodes.back()) {
str.append(". ");
}
size_t s0 = str.length();
append_node_to_string(&child, str, 0, listing_indent + extra_indent);
str.push_back(' ');
extra_indent += (str.length() - s0);
if (&child == &node->child_nodes.at(0) && !child.break_list) {
//
if (child.kind == Node::Kind::LIST) {
c0 = 0;
} else {
c0 = str.length() - s0;
}
}
}
str.pop_back();
if (node->needs_end_paren_newline()) {
str.push_back('\n');
for (int i = 0; i < listing_indent + c0 + 1; i++) {
str.push_back(' ');
}
}
str.push_back(')');
}
break;
default:
ASSERT(false);
}
}
std::string node_to_string(const Node* node) {
std::string result;
append_node_to_string(node, result, 0, 0);
return result;
}
} // namespace v2
std::string to_string(const goos::Object& obj, int line_length) {
using namespace v2;
// construct the tree
Node root = to_node(obj);
// create tree links and order by depth
std::vector<Node*> bfs_order;
root.link(nullptr, &bfs_order, 0);
insert_required_breaks(bfs_order);
// compute subtree lengths
recompute_lengths(bfs_order);
int max_depth = 0;
for (auto node : bfs_order) {
max_depth = std::max((int)node->my_depth, max_depth);
}
int num_broken = 1;
while (num_broken) {
num_broken = run_algorithm(bfs_order, line_length);
}
return node_to_string(&root);
}
} // namespace pretty_print
-14
View File
@@ -1,14 +0,0 @@
#pragma once
#include <string>
#include <vector>
#include "common/goos/Object.h"
#include "common/goos/Printer.h"
#include "common/goos/Reader.h"
namespace pretty_print {
// main pretty print function
std::string to_string(const goos::Object& obj, int line_length = 110);
} // namespace pretty_print
-111
View File
@@ -1,111 +0,0 @@
#include "Printer.h"
#include <cmath>
#include <mutex>
#include "common/goos/Object.h"
#include "common/util/print_float.h"
#include "fmt/core.h"
namespace pretty_print {
namespace {
// the integer representation is used here instead, wouldn't want really long numbers
const std::unordered_set<u32> banned_floats = {};
// print these floats (shown as ints here) as a named constant instead
const std::unordered_map<u32, std::string> const_floats = {{0x40490fda, "PI"},
{0xc0490fda, "MINUS_PI"}};
} // namespace
/*!
* Print a float in a nice representation if possible, or an exact 32-bit integer constant to
* be reinterpreted.
*/
goos::Object float_representation(float value) {
u32 int_value;
memcpy(&int_value, &value, 4);
if (!proper_float(value)) {
// lg::warn("PS2-incompatible float (0x{:08X}) detected! Writing as the-as cast.", int_value);
return pretty_print::build_list("the-as", "float", fmt::format("#x{:x}", int_value));
} else if (const_floats.find(int_value) != const_floats.end()) {
return pretty_print::to_symbol(const_floats.at(int_value));
} else if (banned_floats.find(int_value) == banned_floats.end()) {
return goos::Object::make_float(value);
} else {
return pretty_print::build_list("the-as", "float", fmt::format("#x{:x}", int_value));
}
}
std::unique_ptr<goos::Reader> pretty_printer_reader;
std::mutex pretty_printer_reader_mutex;
goos::Reader& get_pretty_printer_reader() {
if (!pretty_printer_reader) {
pretty_printer_reader = std::make_unique<goos::Reader>();
}
return *pretty_printer_reader;
}
goos::Object to_symbol(const std::string& str) {
std::lock_guard<std::mutex> guard(pretty_printer_reader_mutex);
return goos::Object::make_symbol(&get_pretty_printer_reader().symbolTable, str.c_str());
}
goos::Object new_string(const std::string& str) {
return goos::StringObject::make_new(str);
}
goos::Object build_list(const std::string& str) {
return build_list(to_symbol(str));
}
goos::Object build_list(const goos::Object& obj) {
return goos::PairObject::make_new(obj, goos::Object::make_empty_list());
}
goos::Object build_list(const std::vector<goos::Object>& objects) {
if (objects.empty()) {
return goos::Object::make_empty_list();
} else {
return build_list(objects.data(), objects.size());
}
}
// build a list out of an array of forms
goos::Object build_list(const goos::Object* objects, int count) {
ASSERT(count);
goos::Object result = goos::Object::make_empty_list();
for (int i = count; i-- > 0;) {
result = goos::PairObject::make_new(objects[i], result);
}
return result;
}
// build a list out of a vector of strings that are converted to symbols
goos::Object build_list(const std::vector<std::string>& symbols) {
if (symbols.empty()) {
return goos::Object::make_empty_list();
}
std::vector<goos::Object> f;
f.reserve(symbols.size());
for (auto& x : symbols) {
f.push_back(to_symbol(x));
}
return build_list(f.data(), f.size());
}
void append(goos::Object& _in, const goos::Object& add) {
auto* in = &_in;
while (in->is_pair() && !in->as_pair()->cdr.is_empty_list()) {
in = &in->as_pair()->cdr;
}
if (!in->is_pair()) {
ASSERT(false); // invalid list
}
in->as_pair()->cdr = add;
}
} // namespace pretty_print
-51
View File
@@ -1,51 +0,0 @@
#pragma once
#include <string>
#include <vector>
#include "common/goos/Object.h"
#include "common/goos/Reader.h"
namespace pretty_print {
// string -> object (as a symbol)
goos::Object to_symbol(const std::string& str);
goos::Object new_string(const std::string& str);
// list with a single symbol from a string
goos::Object build_list(const std::string& str);
// wrap an object in a list
goos::Object build_list(const goos::Object& obj);
// build a list out of a vector of forms
goos::Object build_list(const std::vector<goos::Object>& objects);
// build a list out of an array of forms
goos::Object build_list(const goos::Object* objects, int count);
// build a list out of a vector of strings that are converted to symbols
goos::Object build_list(const std::vector<std::string>& symbols);
// fancy wrapper functions. Due to template magic these can call each other
// and accept mixed arguments!
template <typename... Args>
goos::Object build_list(const goos::Object& car, Args... rest);
template <typename... Args>
goos::Object build_list(const std::string& str, Args... rest) {
return goos::PairObject::make_new(to_symbol(str), build_list(rest...));
}
template <typename... Args>
goos::Object build_list(const goos::Object& car, Args... rest) {
return goos::PairObject::make_new(car, build_list(rest...));
}
goos::Reader& get_pretty_printer_reader();
goos::Object float_representation(float value);
void append(goos::Object& _in, const goos::Object& add);
} // namespace pretty_print
+58 -165
View File
@@ -10,12 +10,10 @@
*/
#include "Reader.h"
#include "common/log/log.h"
#include "common/util/FileUtil.h"
#include "common/util/FontUtils.h"
#include "fmt/core.h"
#include "third-party/fmt/core.h"
#include <filesystem>
#include "ReplUtils.h"
namespace goos {
@@ -65,7 +63,6 @@ void TextStream::seek_past_whitespace_and_comments() {
case ' ':
case '\t':
case '\n':
case '\r':
// just a whitespace, eat it!
read();
break;
@@ -78,8 +75,8 @@ void TextStream::seek_past_whitespace_and_comments() {
case '#':
if (text_remains(1) && peek(1) == '|') {
ASSERT(read() == '#'); // #
ASSERT(read() == '|'); // |
assert(read() == '#'); // #
assert(read() == '|'); // |
bool found_end = false;
// find |#
@@ -105,27 +102,6 @@ void TextStream::seek_past_whitespace_and_comments() {
}
}
/*!
* Read encoding bytes on a TextStream and check if it's UTF-8.
* If it's not, you can choose to throw or not.
* If UTF-8 encoding is not detected, the stream is not advanced.
*/
void TextStream::read_utf8_encoding(bool throw_on_error) {
if (text_remains(2)) {
if ((u8)peek(0) == 0xEF && (u8)peek(1) == 0xBB && (u8)peek(2) == 0xBF) {
read();
read();
read();
return;
}
}
if (throw_on_error) {
throw std::runtime_error(
fmt::format("UTF-8 encoding not detected in {}", text->get_description()));
}
}
Reader::Reader() {
// add default macros
add_reader_macro("'", "quote");
@@ -166,34 +142,12 @@ Reader::Reader() {
m_valid_source_text_chars[(int)'\n'] = true;
m_valid_source_text_chars[(int)'\t'] = true;
m_valid_source_text_chars[(int)'\r'] = true;
// allow every character that gets transformed to something else
for (auto& [version, font] : g_font_banks) {
for (auto& remap : *font->encode_info()) {
for (auto rc : remap.chars) {
m_valid_source_text_chars[(u8)rc] = true;
}
}
for (auto& remap : *font->replace_info()) {
for (auto rc : remap.to) {
m_valid_source_text_chars[(u8)rc] = true;
}
for (auto rc : remap.from) {
m_valid_source_text_chars[(u8)rc] = true;
}
}
}
m_valid_source_text_chars[0] = false;
}
bool Reader::is_valid_source_char(char c) const {
return m_valid_source_text_chars[(u8)c];
}
/*!
* Prompt the user and read the result.
*/
std::optional<Object> Reader::read_from_stdin(const std::string& prompt, REPL::Wrapper& repl) {
std::optional<Object> Reader::read_from_stdin(const std::string& prompt, ReplWrapper& repl) {
// escape code will make sure that we remove any color
std::string prompt_full = "\033[0m" + prompt;
@@ -209,7 +163,7 @@ std::optional<Object> Reader::read_from_stdin(const std::string& prompt, REPL::W
db.insert(textFrag);
// perform read
auto result = internal_read(textFrag, false);
auto result = internal_read(textFrag);
db.link(result, textFrag, 0);
return result;
} else {
@@ -220,15 +174,13 @@ std::optional<Object> Reader::read_from_stdin(const std::string& prompt, REPL::W
/*!
* Read a string.
*/
Object Reader::read_from_string(const std::string& str,
bool add_top_level,
const std::optional<std::string>& string_name) {
Object Reader::read_from_string(const std::string& str, bool add_top_level) {
// create text fragment and add to the DB
auto textFrag = std::make_shared<ProgramString>(str, string_name.value_or("Program string"));
auto textFrag = std::make_shared<ProgramString>(str);
db.insert(textFrag);
// perform read
auto result = internal_read(textFrag, false, add_top_level);
auto result = internal_read(textFrag, add_top_level);
db.link(result, textFrag, 0);
return result;
}
@@ -236,18 +188,11 @@ Object Reader::read_from_string(const std::string& str,
/*!
* Read a file
*/
Object Reader::read_from_file(const std::vector<std::string>& file_path, bool check_encoding) {
std::string file_descriptor = fmt::format("{}", fmt::join(file_path, "/"));
const auto joined_file_path = file_util::get_file_path(file_path);
if (!fs::exists(joined_file_path)) {
throw std::runtime_error(fmt::format("Cannot read {}, file doesn't exist", joined_file_path));
}
auto textFrag = std::make_shared<FileText>(joined_file_path, file_descriptor);
Object Reader::read_from_file(const std::vector<std::string>& file_path) {
auto textFrag = std::make_shared<FileText>(file_util::get_file_path(file_path));
db.insert(textFrag);
auto result = internal_read(textFrag, check_encoding);
auto result = internal_read(textFrag);
db.link(result, textFrag, 0);
return result;
}
@@ -255,54 +200,36 @@ Object Reader::read_from_file(const std::vector<std::string>& file_path, bool ch
/*!
* Common read for a SourceText
*/
Object Reader::internal_read(std::shared_ptr<SourceText> text,
bool check_encoding,
bool add_top_level) {
// verify UTF-8 encoding
if (check_encoding && (text->get_size() < 3 || (u8)text->get_text()[0] != 0xEF ||
(u8)text->get_text()[1] != 0xBB || (u8)text->get_text()[2] != 0xBF)) {
throw std::runtime_error(
fmt::format("Text file {} has invalid encoding", text->get_description()));
}
// validate the input
for (int offset = check_encoding ? 3 : 0; offset < text->get_size(); offset++) {
if (!is_valid_source_char(text->get_text()[offset])) {
Object Reader::internal_read(std::shared_ptr<SourceText> text, bool add_top_level) {
// validate the input;
for (int offset = 0; offset < text->get_size(); offset++) {
if (!m_valid_source_text_chars[(u8)text->get_text()[offset]]) {
// failed.
int line_number = text->get_line_idx(offset) + 1;
throw std::runtime_error(fmt::format("Invalid character found on line {} of {}: 0x{:x}",
line_number, text->get_description(),
(u8)text->get_text()[offset]));
(u32)text->get_text()[offset]));
}
}
// first create stream
TextStream ts(text);
if (check_encoding) {
// discard the UTF-8 encoding bytes
ts.read_utf8_encoding(true);
}
// clean up first whitespace
ts.seek_past_whitespace_and_comments();
// read list!
try {
auto objs = read_list(ts, false);
if (add_top_level) {
return PairObject::make_new(Object::make_symbol(&symbolTable, "top-level"), objs);
} else {
return objs;
}
} catch (std::exception& e) {
lg::print("{}", e.what());
throw;
auto objs = read_list(ts, false);
if (add_top_level) {
return PairObject::make_new(SymbolObject::make_new(symbolTable, "top-level"), objs);
} else {
return objs;
}
}
bool Reader::check_string_is_valid(const std::string& str) const {
for (auto c : str) {
if (!is_valid_source_char(c)) {
if (!m_valid_source_text_chars[(u8)c]) {
return false;
}
}
@@ -314,7 +241,7 @@ bool Reader::check_string_is_valid(const std::string& str) const {
* whitespace at the end and leaves the stream on the first character after the token.
*/
Token Reader::get_next_token(TextStream& stream) {
ASSERT(stream.text_remains());
assert(stream.text_remains());
Token t;
t.source_line = stream.line_count;
t.source_offset = stream.seek;
@@ -344,7 +271,7 @@ Token Reader::get_next_token(TextStream& stream) {
// Second - not a special token, so we read until we get a character that ends the token.
while (stream.text_remains()) {
char next = stream.peek();
if (next == ' ' || next == '\n' || next == '\t' || next == '\r' || next == ')' || next == ';' ||
if (next == ' ' || next == '\n' || next == '\t' || next == ')' || next == ';' || next == '#' ||
next == '(') {
return t;
} else {
@@ -393,7 +320,7 @@ bool Reader::read_object(Token& tok, TextStream& ts, Object& obj) {
// try as string
if (tok.text[0] == '"') {
// it's a string.
ASSERT(tok.text.length() == 1);
assert(tok.text.length() == 1);
if (read_string(ts, obj)) {
return true;
} else {
@@ -424,22 +351,22 @@ bool Reader::read_object(Token& tok, TextStream& ts, Object& obj) {
}
bool Reader::read_array(TextStream& stream, Object& o) {
// ASSERT(stream.read() == '(');
// assert(stream.read() == '(');
stream.seek_past_whitespace_and_comments();
std::vector<Object> objects;
bool got_close_paren = false;
while (stream.text_remains()) {
auto tok = get_next_token(stream);
ASSERT(!tok.text.empty());
assert(!tok.text.empty());
if (tok.text[0] == '(') {
ASSERT(tok.text.length() == 1);
assert(tok.text.length() == 1);
objects.push_back(read_list(stream, true));
stream.seek_past_whitespace_and_comments();
continue;
} else if (tok.text[0] == ')') {
ASSERT(tok.text.length() == 1);
assert(tok.text.length() == 1);
got_close_paren = true;
break;
} else {
@@ -463,51 +390,12 @@ bool Reader::read_array(TextStream& stream, Object& o) {
return true;
}
struct ListBuilder {
Object head;
std::shared_ptr<PairObject> prev_tail;
std::shared_ptr<PairObject> tail;
int size = 0;
ListBuilder() { head = Object::make_empty_list(); }
void push_back(Object&& o) {
size++;
if (!tail) {
tail = std::make_shared<PairObject>(o, Object{});
head.type = ObjectType::PAIR;
head.heap_obj = tail;
} else {
auto next = std::make_shared<PairObject>(o, Object{});
tail->cdr.type = ObjectType::PAIR;
tail->cdr.heap_obj = next;
prev_tail = std::move(tail);
tail = std::move(next);
}
}
Object pop_back() {
auto obj = tail->car;
tail = std::move(prev_tail);
return obj;
}
void finalize() {
if (tail) {
tail->cdr = Object::make_empty_list();
} else {
head = Object::make_empty_list();
}
}
};
/*!
* Call this on the character after the open paren.
*/
Object Reader::read_list(TextStream& ts, bool expect_close_paren) {
ts.seek_past_whitespace_and_comments();
// std::vector<Object> objects;
ListBuilder list_builder;
std::vector<Object> objects;
bool got_close_paren = false; // does this list end?
bool got_dot = false; // did we get a . ?
@@ -549,23 +437,22 @@ Object Reader::read_list(TextStream& ts, bool expect_close_paren) {
}
// inserter function, used to properly insert a next object
auto insert_object = [&](Object&& o) {
auto insert_object = [&](const Object& o) {
if (got_thing_after_dot) {
throw_reader_error(ts, "A list cannot have multiple entries after the dot", -1);
}
if (reader_macro_string_stack.empty()) {
list_builder.push_back(std::move(o));
// objects.push_back(o);
objects.push_back(o);
} else {
Object to_push_back = std::move(o);
Object to_push_back = o;
while (!reader_macro_string_stack.empty()) {
to_push_back = build_list(
{Object::make_symbol(&symbolTable, reader_macro_string_stack.back().c_str()),
to_push_back});
to_push_back =
build_list({SymbolObject::make_new(symbolTable, reader_macro_string_stack.back()),
to_push_back});
reader_macro_string_stack.pop_back();
}
list_builder.push_back(std::move(to_push_back));
objects.push_back(to_push_back);
}
// remember if we got an object after the dot
@@ -575,19 +462,19 @@ Object Reader::read_list(TextStream& ts, bool expect_close_paren) {
};
if (tok.text.empty()) {
ASSERT(false);
assert(false);
// empty list
break;
} else if (tok.text[0] == '(') {
// nested list
ASSERT(tok.text.length() == 1);
assert(tok.text.length() == 1);
insert_object(read_list(ts, true));
ts.seek_past_whitespace_and_comments();
continue;
} else if (tok.text[0] == ')') {
// end of this list
got_close_paren = true;
ASSERT(tok.text.length() == 1);
assert(tok.text.length() == 1);
break;
} else {
// try to get an object
@@ -595,7 +482,7 @@ Object Reader::read_list(TextStream& ts, bool expect_close_paren) {
if (read_object(tok, ts, obj)) {
ts.seek_past_whitespace_and_comments();
insert_object(std::move(obj));
insert_object(obj);
} else {
throw_reader_error(ts, "invalid token encountered in reader: " + tok.text,
-int(tok.text.size()));
@@ -618,12 +505,12 @@ Object Reader::read_list(TextStream& ts, bool expect_close_paren) {
// build up list or improper list, link it, and return!
if (got_thing_after_dot) {
if (list_builder.size < 2) {
if (objects.size() < 2) {
throw_reader_error(ts, "A list with a dot must have at least one thing before the dot", -1);
}
auto back = list_builder.pop_back();
list_builder.finalize();
auto rv = list_builder.head;
auto back = objects.back();
objects.pop_back();
auto rv = build_list(objects);
auto lst = rv;
while (true) {
@@ -637,8 +524,7 @@ Object Reader::read_list(TextStream& ts, bool expect_close_paren) {
db.link(rv, ts.text, start_offset);
return rv;
} else {
list_builder.finalize();
auto rv = list_builder.head;
auto rv = build_list(objects);
db.link(rv, ts.text, start_offset);
return rv;
}
@@ -649,10 +535,10 @@ Object Reader::read_list(TextStream& ts, bool expect_close_paren) {
*/
bool Reader::try_token_as_symbol(const Token& tok, Object& obj) {
// check start character is valid:
ASSERT(!tok.text.empty());
assert(!tok.text.empty());
char start = tok.text[0];
if (m_valid_symbols_chars[(int)start]) {
obj = Object::make_symbol(&symbolTable, tok.text.c_str());
obj = SymbolObject::make_new(symbolTable, tok.text);
return true;
} else {
return false;
@@ -708,7 +594,7 @@ bool Reader::read_string(TextStream& stream, Object& obj) {
if (end != 2) {
throw_reader_error(stream, "invalid character escape", -2);
}
ASSERT(value < 256);
assert(value < 256);
str.push_back(char(value));
} else {
throw_reader_error(stream, "unknown string escape code", -1);
@@ -880,6 +766,13 @@ void Reader::throw_reader_error(TextStream& here, const std::string& err, int se
db.get_info_for(here.text, here.seek + seek_offset));
}
/*!
* Get the source directory of the current project.
*/
std::string Reader::get_source_dir() {
return file_util::get_project_path();
}
/*!
* Convert any string into one that can be read.
* Unprintable characters become escape sequences, including tab and newline.
+13 -18
View File
@@ -12,14 +12,15 @@
*/
#include <memory>
#include <optional>
#include <unordered_map>
#include "common/util/assert.h"
#include <utility>
#include <unordered_map>
#include <optional>
#include "common/goos/Object.h"
#include "common/goos/TextDB.h"
#include "common/repl/repl_wrapper.h"
#include "common/util/Assert.h"
#include "ReplUtils.h"
namespace goos {
@@ -34,17 +35,17 @@ struct TextStream {
int line_count = 0;
char peek() {
ASSERT(seek < text->get_size());
assert(seek < text->get_size());
return text->get_text()[seek];
}
char peek(int i) {
ASSERT(seek + i < text->get_size());
assert(seek + i < text->get_size());
return text->get_text()[seek + i];
}
char read() {
ASSERT(seek < text->get_size());
assert(seek < text->get_size());
char c = text->get_text()[seek++];
if (c == '\n')
line_count++;
@@ -54,7 +55,6 @@ struct TextStream {
bool text_remains() { return seek < text->get_size(); }
bool text_remains(int i) { return seek + i < text->get_size(); }
void seek_past_whitespace_and_comments();
void read_utf8_encoding(bool throw_on_error);
};
/*!
@@ -70,20 +70,17 @@ struct Token {
class Reader {
public:
Reader();
Object read_from_string(const std::string& str,
bool add_top_level = true,
const std::optional<std::string>& string_name = {});
std::optional<Object> read_from_stdin(const std::string& prompt, REPL::Wrapper& repl);
Object read_from_file(const std::vector<std::string>& file_path, bool check_encoding = false);
Object read_from_string(const std::string& str, bool add_top_level = true);
std::optional<Object> read_from_stdin(const std::string& prompt, ReplWrapper& repl);
Object read_from_file(const std::vector<std::string>& file_path);
bool check_string_is_valid(const std::string& str) const;
std::string get_source_dir();
SymbolTable symbolTable;
TextDb db;
private:
Object internal_read(std::shared_ptr<SourceText> text,
bool check_encoding,
bool add_top_level = true);
Object internal_read(std::shared_ptr<SourceText> text, bool add_top_level = true);
Object read_list(TextStream& stream, bool expect_close_paren = true);
bool read_object(Token& tok, TextStream& ts, Object& obj);
bool read_array(TextStream& stream, Object& o);
@@ -103,8 +100,6 @@ class Reader {
bool m_valid_symbols_chars[256];
bool m_valid_source_text_chars[256];
bool is_valid_source_char(char c) const;
std::unordered_map<std::string, std::string> m_reader_macros;
};
+137
View File
@@ -0,0 +1,137 @@
#include "ReplUtils.h"
#include "common/util/FileUtil.h"
#include "third-party/replxx/include/replxx.hxx"
#include "common/versions.h"
#include "third-party/fmt/color.h"
#include "third-party/fmt/core.h"
// TODO - expand a list of hints (ie. a hint for defun to show at a glance how to write a function,
// or perhaps, show the docstring for the current function being used?)
using Replxx = replxx::Replxx;
void ReplWrapper::clear_screen() {
repl.clear_screen();
}
void ReplWrapper::print_welcome_message() {
// Welcome message / brief intro for documentation
std::string ascii;
ascii += " _____ _____ _____ _____ __ \n";
ascii += "| |___ ___ ___| __| | _ | | \n";
ascii += "| | | . | -_| | | | | | | |__ \n";
ascii += "|_____| _|___|_|_|_____|_____|__|__|_____|\n";
ascii += " |_| \n";
fmt::print(fmt::emphasis::bold | fg(fmt::color::orange), ascii);
fmt::print("Welcome to OpenGOAL {}.{}!\n", versions::GOAL_VERSION_MAJOR,
versions::GOAL_VERSION_MINOR);
fmt::print("Run ");
fmt::print(fmt::emphasis::bold | fg(fmt::color::cyan), "(repl-help)");
fmt::print(" for help with common commands and REPL usage.\n");
fmt::print("Run ");
fmt::print(fmt::emphasis::bold | fg(fmt::color::cyan), "(lt)");
fmt::print(" to connect to the local target.\n\n");
}
void ReplWrapper::set_history_max_size(size_t len) {
repl.set_max_history_size(len);
}
const char* ReplWrapper::readline(const std::string& prompt) {
return repl.input(prompt);
}
void ReplWrapper::add_to_history(const std::string& line) {
repl.history_add(line);
}
void ReplWrapper::save_history() {
// NOTE - library doesn't seem unicode safe on windows
std::filesystem::path path = file_util::get_user_home_dir();
if (std::filesystem::exists(path)) {
repl.history_save((path / ".opengoal.repl.history").string());
}
}
void ReplWrapper::load_history() {
// NOTE - library doesn't seem unicode safe on windows
std::filesystem::path path = file_util::get_user_home_dir();
if (std::filesystem::exists(path)) {
repl.history_load((path / ".opengoal.repl.history").string());
}
}
std::pair<std::string, bool> ReplWrapper::get_current_repl_token(std::string const& context) {
// Find the current token
std::string token = "";
for (auto c = context.crbegin(); c != context.crend(); c++) {
if (std::isspace(*c)) {
break;
} else {
token = *c + token;
}
}
// If there is a preceeding '(' remove it
if (!token.empty() && token.at(0) == '(') {
token.erase(0, 1);
return {token, true};
}
return {token, false};
}
void ReplWrapper::print_help_message() {
fmt::print(fmt::emphasis::bold, "\nREPL Controls:\n");
fmt::print(fmt::emphasis::bold | fg(fmt::color::cyan), "(:clear)\n");
fmt::print(" - Clear the current screen\n");
fmt::print(fmt::emphasis::bold | fg(fmt::color::cyan), "(e)\n");
fmt::print(" - Exit the compiler once the current REPL command is finished\n");
fmt::print(fmt::emphasis::bold | fg(fmt::color::cyan), "(lt [ip-address] [port-number])\n");
fmt::print(
" - Connect the listener to a running target. The IP address defaults to `127.0.0.1` and the "
"port to `8112`\n");
fmt::print(fmt::emphasis::bold | fg(fmt::color::cyan), "(r [ip-address] [port-number])\n");
fmt::print(
" - Attempt to reset the target and reconnect. After this, the target will have nothing "
"loaded.\n");
fmt::print(fmt::emphasis::bold | fg(fmt::color::cyan), "(:status)\n");
fmt::print(" - Send a ping-like message to the target. Requires the target to be connected\n");
fmt::print(fmt::emphasis::bold | fg(fmt::color::cyan), "(shutdown-target)\n");
fmt::print(" - If the target is connected, make it exit\n");
fmt::print(fmt::emphasis::bold, "\nCompiling & Building:\n");
fmt::print(fmt::emphasis::bold | fg(fmt::color::lime_green), "(m \"filename\")\n");
fmt::print(" - Compile an OpenGOAL source file\n");
fmt::print(fmt::emphasis::bold | fg(fmt::color::lime_green), "(ml \"filename\")\n");
fmt::print(" - Compile and Load an OpenGOAL source file\n");
fmt::print(fmt::emphasis::bold | fg(fmt::color::lime_green), "(build-game)\n");
fmt::print(" - Loads and builds all game files and rebuilds DGOs\n");
fmt::print(fmt::emphasis::bold | fg(fmt::color::lime_green), "(build-kernel)\n");
fmt::print(" - Similar to (build-game) but only the kernel files\n");
fmt::print(fmt::emphasis::bold | fg(fmt::color::lime_green), "(blg)\n");
fmt::print(" - Performs a (build-game) and then loads all CGOs\n");
fmt::print(fmt::emphasis::bold | fg(fmt::color::lime_green),
"(build-dgos \"path/to/dgos/description/file\")\n");
fmt::print(
" - Builds all the DGO files described in the DGO description file. See "
"`goal_src/builds/dgos.txt` for an example.\n");
fmt::print(fmt::emphasis::bold | fg(fmt::color::lime_green),
"(asm-data-file tool-name \"file-name\")\n");
fmt::print(
" - Build a data file. The `tool-name` refers to which data building tool should be used.\n");
fmt::print(fmt::emphasis::bold | fg(fmt::color::lime_green), "(build-data)\n");
fmt::print(" - Macro for rebuilding all data files\n");
fmt::print(fmt::emphasis::bold, "\nOther:\n");
fmt::print(fmt::emphasis::bold | fg(fmt::color::magenta), "(gs)\n");
fmt::print(" - Enter a GOOS REPL\n");
fmt::print(fmt::emphasis::bold | fg(fmt::color::magenta),
"(set-config! config-name config-value)\n");
fmt::print(" - Used to set compiler configuration\n");
}
void ReplWrapper::init_default_settings() {
repl.set_word_break_characters(" \t");
}
+33
View File
@@ -0,0 +1,33 @@
#pragma once
#include <functional>
#include <string>
#include <vector>
#include "third-party/replxx/include/replxx.hxx"
using Replxx = replxx::Replxx;
class ReplWrapper {
Replxx repl;
public:
ReplWrapper() {}
Replxx& get_repl() { return repl; }
void init_default_settings();
// Functionality / Commands
void clear_screen();
void print_welcome_message();
void set_history_max_size(size_t len);
const char* readline(const std::string& prompt);
void add_to_history(const std::string& line);
void save_history();
void load_history();
void print_help_message();
std::pair<std::string, bool> get_current_repl_token(std::string const& context);
std::vector<std::string> examples{};
using cl = Replxx::Color;
std::vector<std::pair<std::string, cl>> regex_colors{};
};
Loaded 100 of 15875 files, more files were not shown because too many files have changed in this diff. Show more