Files
jak-project/game/graphics/opengl_renderer/sprite/Sprite3.cpp
T
Tyler Wilding d3cc739e43 jakx: Commit existing work from other PRs (#4112)
This attempts to get into master whatever work was done in this PR /
it's earlier PR https://github.com/open-goal/jak-project/pull/3965

I don't want this work to be lost / floating around in massive PRs.

However the changes are:
- switch to ntsc_v1 instead of PAL as the development target, as we have
done for all other games
- remove most of the copied-from-jak2/3 changes as they need to be
confirmed during the decompilation process not just assumed
- avoids committing any changes to `game/kernel/common` as it was not
clear to me if these were changes made in jak x's kernel that were not
properly broken out into it's own functions. We don't want to
accidentally introduce bugs into jak1-3's kernel code.
- in other words, if the change in the kernel only happens in jak x...it
should likely be specific to jak x's kernel, not common.

---------

Co-authored-by: VodBox <dillon@vodbox.io>
Co-authored-by: yodah <greenboyyodah@gmail.com>
2025-12-31 21:08:44 -05:00

906 lines
33 KiB
C++

#include "Sprite3.h"
#include "common/log/log.h"
#include "game/graphics/opengl_renderer/background/background_common.h"
#include "game/graphics/opengl_renderer/dma_helpers.h"
#include "fmt/format.h"
#include "third-party/imgui/imgui.h"
namespace {
/*!
* Does the next DMA transfer look like it could be the start of a 2D group?
*/
bool looks_like_2d_chunk_start(const DmaFollower& dma) {
return dma.current_tag().qwc == 1 && dma.current_tag().kind == DmaTag::Kind::CNT;
}
/*!
* Read the header. Asserts if it's bad.
* Returns the number of sprites.
* Advances 1 dma transfer
*/
u32 process_sprite_chunk_header(DmaFollower& dma) {
auto transfer = dma.read_and_advance();
// note that flg = true, this should use double buffering
bool ok = verify_unpack_with_stcycl(transfer, VifCode::Kind::UNPACK_V4_32, 4, 4, 1,
SpriteDataMem::Header, false, true);
ASSERT(ok);
u32 header[4];
memcpy(header, transfer.data, 16);
ASSERT(header[0] <= Sprite3::SPRITES_PER_CHUNK);
return header[0];
}
constexpr int SPRITE_RENDERER_MAX_SPRITES = 1920 * 12;
} // namespace
Sprite3::Sprite3(const std::string& name, int my_id)
: BucketRenderer(name, my_id), m_direct(name, my_id, 1024) {
opengl_setup();
}
void Sprite3::opengl_setup() {
// Set up OpenGL for 'normal' sprites
opengl_setup_normal();
// Set up OpenGL for distort sprites
opengl_setup_distort();
}
void Sprite3::opengl_setup_normal() {
glGenBuffers(1, &m_ogl.vertex_buffer);
glGenVertexArrays(1, &m_ogl.vao);
glBindVertexArray(m_ogl.vao);
glBindBuffer(GL_ARRAY_BUFFER, m_ogl.vertex_buffer);
auto verts = SPRITE_RENDERER_MAX_SPRITES * 4;
auto bytes = verts * sizeof(SpriteVertex3D);
glBufferData(GL_ARRAY_BUFFER, bytes, nullptr, GL_STREAM_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(
0, // location 0 in the shader
4, // 4 floats per vert (w unused)
GL_FLOAT, // floats
GL_TRUE, // normalized, ignored,
sizeof(SpriteVertex3D), //
(void*)offsetof(SpriteVertex3D, xyz_sx) // offset in array (why is this a pointer...)
);
glEnableVertexAttribArray(1);
glVertexAttribPointer(
1, // location 1 in the shader
4, // 4 color components
GL_FLOAT, // floats
GL_TRUE, // normalized, ignored,
sizeof(SpriteVertex3D), //
(void*)offsetof(SpriteVertex3D, quat_sy) // offset in array (why is this a pointer...)
);
glEnableVertexAttribArray(2);
glVertexAttribPointer(
2, // location 2 in the shader
4, // 4 color components
GL_FLOAT, // floats
GL_TRUE, // normalized, ignored,
sizeof(SpriteVertex3D), //
(void*)offsetof(SpriteVertex3D, rgba) // offset in array (why is this a pointer...)
);
glEnableVertexAttribArray(3);
glVertexAttribIPointer(
3, // location 3 in the shader
2, // 4 color components
GL_UNSIGNED_SHORT, // floats
sizeof(SpriteVertex3D), //
(void*)offsetof(SpriteVertex3D, flags_matrix) // offset in array (why is this a pointer...)
);
glEnableVertexAttribArray(4);
glVertexAttribIPointer(
4, // location 4 in the shader
4, // 3 floats per vert
GL_UNSIGNED_SHORT, // floats
sizeof(SpriteVertex3D), //
(void*)offsetof(SpriteVertex3D, info) // offset in array (why is this a pointer...)
);
glBindBuffer(GL_ARRAY_BUFFER, 0);
u32 idx_buffer_len = SPRITE_RENDERER_MAX_SPRITES * 5;
glGenBuffers(1, &m_ogl.index_buffer);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ogl.index_buffer);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, idx_buffer_len * sizeof(u32), nullptr, GL_STREAM_DRAW);
glBindVertexArray(0);
m_vertices_3d.resize(verts);
m_index_buffer_data.resize(idx_buffer_len);
m_default_mode.disable_depth_write();
m_default_mode.set_depth_test(GsTest::ZTest::GEQUAL);
m_default_mode.set_alpha_blend(DrawMode::AlphaBlend::SRC_DST_SRC_DST);
m_default_mode.set_aref(38);
m_default_mode.set_alpha_test(DrawMode::AlphaTest::GEQUAL);
m_default_mode.set_alpha_fail(GsTest::AlphaFail::FB_ONLY);
m_default_mode.set_at(true);
m_default_mode.set_zt(true);
m_default_mode.set_ab(true);
m_current_mode = m_default_mode;
}
/*!
* Handle DMA data that does the per-frame setup.
* This should get the dma chain immediately after the call to sprite-draw-distorters.
* It ends right before the sprite-add-matrix-data for the 3d's
*/
void Sprite3::handle_sprite_frame_setup(DmaFollower& dma,
GameVersion version,
SharedRenderState* render_state,
ScopedProfilerNode& /*prof*/) {
// first is some direct data
auto direct_data = dma.read_and_advance();
ASSERT(direct_data.size_bytes == 3 * 16);
memcpy(m_sprite_direct_setup, direct_data.data, 3 * 16);
ASSERT(m_sprite_direct_setup[0] == 0x2000000000008001);
ASSERT(m_sprite_direct_setup[1] == 0xEEEEEEEEEEEEEEEE);
ASSERT(m_sprite_direct_setup[2] == 0x000000000005126B);
ASSERT(m_sprite_direct_setup[3] == 0x0000000000000047);
ASSERT(m_sprite_direct_setup[4] == 0x0000000000000005);
ASSERT(m_sprite_direct_setup[5] == 0x0000000000000008);
// next would be the program, but it's 0 size on the PC and isn't sent.
// next is the "frame data"
switch (version) {
case GameVersion::Jak1: {
render_state->shaders[ShaderId::SPRITE3].activate();
auto frame_data = dma.read_and_advance();
ASSERT(frame_data.size_bytes == (int)sizeof(SpriteFrameDataJak1)); // very cool
ASSERT(frame_data.vifcode0().kind == VifCode::Kind::STCYCL);
VifCodeStcycl frame_data_stcycl(frame_data.vifcode0());
ASSERT(frame_data_stcycl.cl == 4);
ASSERT(frame_data_stcycl.wl == 4);
ASSERT(frame_data.vifcode1().kind == VifCode::Kind::UNPACK_V4_32);
VifCodeUnpack frame_data_unpack(frame_data.vifcode1());
ASSERT(frame_data_unpack.addr_qw == SpriteDataMem::FrameData);
ASSERT(frame_data_unpack.use_tops_flag == false);
SpriteFrameDataJak1 jak1_data;
memcpy(&jak1_data, frame_data.data, sizeof(SpriteFrameDataJak1));
m_frame_data.from_jak1(jak1_data);
} break;
case GameVersion::Jak2:
case GameVersion::Jak3:
case GameVersion::JakX: {
render_state->shaders[ShaderId::SPRITE3].activate();
auto frame_data = dma.read_and_advance();
ASSERT(frame_data.size_bytes == (int)sizeof(SpriteFrameData)); // very cool
ASSERT(frame_data.vifcode0().kind == VifCode::Kind::STCYCL);
VifCodeStcycl frame_data_stcycl(frame_data.vifcode0());
ASSERT(frame_data_stcycl.cl == 4);
ASSERT(frame_data_stcycl.wl == 4);
ASSERT(frame_data.vifcode1().kind == VifCode::Kind::UNPACK_V4_32);
VifCodeUnpack frame_data_unpack(frame_data.vifcode1());
ASSERT(frame_data_unpack.addr_qw == SpriteDataMem::FrameData);
ASSERT(frame_data_unpack.use_tops_flag == false);
memcpy(&m_frame_data, frame_data.data, sizeof(SpriteFrameData));
} break;
default:
ASSERT_NOT_REACHED();
}
// next, a MSCALF.
auto mscalf = dma.read_and_advance();
ASSERT(mscalf.size_bytes == 0);
ASSERT(mscalf.vifcode0().kind == VifCode::Kind::MSCALF);
ASSERT(mscalf.vifcode0().immediate == SpriteProgMem::Init);
ASSERT(mscalf.vifcode1().kind == VifCode::Kind::FLUSHE);
// next base and offset
auto base_offset = dma.read_and_advance();
ASSERT(base_offset.size_bytes == 0);
ASSERT(base_offset.vifcode0().kind == VifCode::Kind::BASE);
ASSERT(base_offset.vifcode0().immediate == SpriteDataMem::Buffer0);
ASSERT(base_offset.vifcode1().kind == VifCode::Kind::OFFSET);
ASSERT(base_offset.vifcode1().immediate == SpriteDataMem::Buffer1);
}
void Sprite3::render_3d(DmaFollower& dma) {
// one time matrix data
auto matrix_data = dma.read_and_advance();
ASSERT(matrix_data.size_bytes == sizeof(Sprite3DMatrixData));
bool unpack_ok = verify_unpack_with_stcycl(matrix_data, VifCode::Kind::UNPACK_V4_32, 4, 4, 5,
SpriteDataMem::Matrix, false, false);
ASSERT(unpack_ok);
static_assert(sizeof(m_3d_matrix_data) == 5 * 16);
memcpy(&m_3d_matrix_data, matrix_data.data, sizeof(m_3d_matrix_data));
// TODO
}
void Sprite3::render_2d_group0(DmaFollower& dma,
SharedRenderState* render_state,
ScopedProfilerNode& prof) {
// opengl sprite frame setup
auto shid = render_state->shaders[ShaderId::SPRITE3].id();
glUniform4fv(glGetUniformLocation(shid, "hvdf_offset"), 1, m_3d_matrix_data.hvdf_offset.data());
glUniform1f(glGetUniformLocation(shid, "pfog0"), m_frame_data.pfog0);
glUniform1f(glGetUniformLocation(shid, "min_scale"), m_frame_data.min_scale);
glUniform1f(glGetUniformLocation(shid, "max_scale"), m_frame_data.max_scale);
glUniform1f(glGetUniformLocation(shid, "fog_min"), m_frame_data.fog_min);
glUniform1f(glGetUniformLocation(shid, "fog_max"), m_frame_data.fog_max);
// glUniform1f(glGetUniformLocation(shid, "bonus"), m_frame_data.bonus);
// glUniform4fv(glGetUniformLocation(shid, "hmge_scale"), 1, m_frame_data.hmge_scale.data());
glUniform1f(glGetUniformLocation(shid, "deg_to_rad"), m_frame_data.deg_to_rad);
glUniform1f(glGetUniformLocation(shid, "inv_area"), m_frame_data.inv_area);
glUniformMatrix4fv(glGetUniformLocation(shid, "camera"), 1, GL_FALSE,
m_3d_matrix_data.camera.data());
glUniform4fv(glGetUniformLocation(shid, "xy_array"), 8, m_frame_data.xy_array[0].data());
glUniform4fv(glGetUniformLocation(shid, "xyz_array"), 4, m_frame_data.xyz_array[0].data());
glUniform4fv(glGetUniformLocation(shid, "st_array"), 4, m_frame_data.st_array[0].data());
glUniform4fv(glGetUniformLocation(shid, "basis_x"), 1, m_frame_data.basis_x.data());
glUniform4fv(glGetUniformLocation(shid, "basis_y"), 1, m_frame_data.basis_y.data());
u16 last_prog = -1;
while (looks_like_2d_chunk_start(dma)) {
m_debug_stats.blocks_2d_grp0++;
// 4 packets per chunk
// first is the header
u32 sprite_count = process_sprite_chunk_header(dma);
m_debug_stats.count_2d_grp0 += sprite_count;
// second is the vector data
u32 expected_vec_size = sizeof(SpriteVecData2d) * sprite_count;
auto vec_data = dma.read_and_advance();
ASSERT(expected_vec_size <= sizeof(m_vec_data_2d));
unpack_to_no_stcycl(&m_vec_data_2d, vec_data, VifCode::Kind::UNPACK_V4_32, expected_vec_size,
SpriteDataMem::Vector, false, true);
// third is the adgif data
u32 expected_adgif_size = sizeof(AdGifData) * sprite_count;
auto adgif_data = dma.read_and_advance();
ASSERT(expected_adgif_size <= sizeof(m_adgif));
unpack_to_no_stcycl(&m_adgif, adgif_data, VifCode::Kind::UNPACK_V4_32, expected_adgif_size,
SpriteDataMem::Adgif, false, true);
// fourth is the actual run!!!!!
auto run = dma.read_and_advance();
ASSERT(run.vifcode0().kind == VifCode::Kind::NOP);
ASSERT(run.vifcode1().kind == VifCode::Kind::MSCAL);
if (m_enabled) {
if (run.vifcode1().immediate != last_prog) {
// one-time setups and flushing
flush_sprites(render_state, prof, false);
}
if (run.vifcode1().immediate == SpriteProgMem::Sprites2dGrp0) {
if (m_2d_enable) {
do_block_common(SpriteMode::Mode2D, sprite_count, render_state, prof);
}
} else {
if (m_3d_enable) {
do_block_common(SpriteMode::Mode3D, sprite_count, render_state, prof);
}
}
last_prog = run.vifcode1().immediate;
}
}
}
/*!
* Run the pre-sprite directrenderer.
*/
bool Sprite3::render_direct(DmaFollower& dma,
SharedRenderState* render_state,
ScopedProfilerNode& prof) {
m_direct.reset_state();
while (dma.current_tag().qwc != 7 && dma.current_tag_offset() != render_state->next_bucket) {
auto direct_data = dma.read_and_advance();
m_direct.render_vif(direct_data.vif0(), direct_data.vif1(), direct_data.data,
direct_data.size_bytes, render_state, prof);
}
m_direct.flush_pending(render_state, prof);
// if sprites are off, after all the directrenderer dma, there is nothing left and we must exit
if (dma.current_tag_offset() == render_state->next_bucket) {
return true;
}
return false;
}
void Sprite3::render_fake_shadow(DmaFollower& dma) {
// TODO
// nop + flushe
auto nop_flushe = dma.read_and_advance();
ASSERT(nop_flushe.vifcode0().kind == VifCode::Kind::NOP);
ASSERT(nop_flushe.vifcode1().kind == VifCode::Kind::FLUSHE);
}
/*!
* Handle DMA data for group1 2d's (HUD)
*/
void Sprite3::render_2d_group1(DmaFollower& dma,
SharedRenderState* render_state,
ScopedProfilerNode& prof) {
// one time matrix data upload
auto mat_upload = dma.read_and_advance();
bool mat_ok = verify_unpack_with_stcycl(mat_upload, VifCode::Kind::UNPACK_V4_32, 4, 4, 80,
SpriteDataMem::Matrix, false, false);
ASSERT(mat_ok);
ASSERT(mat_upload.size_bytes == sizeof(m_hud_matrix_data));
memcpy(&m_hud_matrix_data, mat_upload.data, sizeof(m_hud_matrix_data));
// opengl sprite frame setup
glUniform4fv(
glGetUniformLocation(render_state->shaders[ShaderId::SPRITE3].id(), "hud_hvdf_offset"), 1,
m_hud_matrix_data.hvdf_offset.data());
glUniform4fv(glGetUniformLocation(render_state->shaders[ShaderId::SPRITE3].id(), "hud_hvdf_user"),
75, m_hud_matrix_data.user_hvdf[0].data());
glUniformMatrix4fv(
glGetUniformLocation(render_state->shaders[ShaderId::SPRITE3].id(), "hud_matrix"), 1,
GL_FALSE, m_hud_matrix_data.matrix.data());
// loop through chunks.
while (looks_like_2d_chunk_start(dma)) {
m_debug_stats.blocks_2d_grp1++;
// 4 packets per chunk
// first is the header
u32 sprite_count = process_sprite_chunk_header(dma);
m_debug_stats.count_2d_grp1 += sprite_count;
// second is the vector data
u32 expected_vec_size = sizeof(SpriteVecData2d) * sprite_count;
auto vec_data = dma.read_and_advance();
ASSERT(expected_vec_size <= sizeof(m_vec_data_2d));
unpack_to_no_stcycl(&m_vec_data_2d, vec_data, VifCode::Kind::UNPACK_V4_32, expected_vec_size,
SpriteDataMem::Vector, false, true);
// third is the adgif data
u32 expected_adgif_size = sizeof(AdGifData) * sprite_count;
auto adgif_data = dma.read_and_advance();
ASSERT(expected_adgif_size <= sizeof(m_adgif));
unpack_to_no_stcycl(&m_adgif, adgif_data, VifCode::Kind::UNPACK_V4_32, expected_adgif_size,
SpriteDataMem::Adgif, false, true);
// fourth is the actual run!!!!!
auto run = dma.read_and_advance();
ASSERT(run.vifcode0().kind == VifCode::Kind::NOP);
ASSERT(run.vifcode1().kind == VifCode::Kind::MSCAL);
switch (render_state->version) {
case GameVersion::Jak1:
ASSERT(run.vifcode1().immediate == SpriteProgMem::Sprites2dHud_Jak1);
break;
case GameVersion::Jak2:
ASSERT(run.vifcode1().immediate == SpriteProgMem::Sprites2dHud_Jak2);
break;
case GameVersion::Jak3:
case GameVersion::JakX:
ASSERT_EQ_IMM(run.vifcode1().immediate, (int)SpriteProgMem::Sprites2dHud_Jak3);
break;
default:
ASSERT_NOT_REACHED();
}
if (m_enabled && m_2d_enable) {
do_block_common(SpriteMode::ModeHUD, sprite_count, render_state, prof);
}
}
}
void Sprite3::render(DmaFollower& dma, SharedRenderState* render_state, ScopedProfilerNode& prof) {
switch (render_state->version) {
case GameVersion::Jak1:
render_jak1(dma, render_state, prof);
break;
case GameVersion::Jak2:
case GameVersion::Jak3:
case GameVersion::JakX:
render_jak2(dma, render_state, prof);
break;
default:
ASSERT_NOT_REACHED();
}
}
void Sprite3::render_jak2(DmaFollower& dma,
SharedRenderState* render_state,
ScopedProfilerNode& prof) {
m_debug_stats = {};
auto data0 = dma.read_and_advance();
ASSERT(data0.vif0() == 0 || data0.vifcode0().kind == VifCode::Kind::MARK);
ASSERT(data0.vif1() == 0 || data0.vifcode1().kind == VifCode::Kind::NOP);
ASSERT(data0.size_bytes == 0);
if (dma.current_tag_offset() == render_state->next_bucket) {
return;
}
// Before anything else, some directrenderer DMA might have been sent
{
auto child = prof.make_scoped_child("direct");
if (render_direct(dma, render_state, child)) {
return;
}
}
// First is the distorter
{
auto child = prof.make_scoped_child("distorter");
render_distorter(dma, render_state, child);
}
// next, the normal sprite stuff
handle_sprite_frame_setup(dma, render_state->version, render_state, prof);
// 3d sprites
render_3d(dma);
// 2d draw
// m_sprite_renderer.reset_state();
{
auto child = prof.make_scoped_child("2d-group0");
render_2d_group0(dma, render_state, child);
flush_sprites(render_state, prof, false);
}
// shadow draw
render_fake_shadow(dma);
// 2d draw (HUD)
{
auto child = prof.make_scoped_child("2d-group1");
render_2d_group1(dma, render_state, child);
flush_sprites(render_state, prof, true);
auto nop_flushe = dma.read_and_advance();
ASSERT(nop_flushe.vifcode0().kind == VifCode::Kind::NOP);
ASSERT(nop_flushe.vifcode1().kind == VifCode::Kind::FLUSHE);
}
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glBlendEquation(GL_FUNC_ADD);
{
auto p = prof.make_scoped_child("glow");
glow_dma_and_draw(dma, render_state, p);
}
// fmt::print("next bucket is 0x{}\n", render_state->next_bucket);
while (dma.current_tag_offset() != render_state->next_bucket) {
// auto tag = dma.current_tag();
auto data = dma.read_and_advance();
(void)data;
// VifCode code(data.vif0());
// fmt::print("@ 0x{:x} tag: {}", dma.current_tag_offset(), tag.print());
// fmt::print(" vif0: {}\n", code.print());
// fmt::print(" vif1: {}\n", VifCode(data.vif1()).print());
}
}
void Sprite3::render_jak1(DmaFollower& dma,
SharedRenderState* render_state,
ScopedProfilerNode& prof) {
m_debug_stats = {};
// First thing should be a NEXT with two nops. this is a jump from buckets to sprite data
auto data0 = dma.read_and_advance();
ASSERT(data0.vif1() == 0);
ASSERT(data0.vif0() == 0);
ASSERT(data0.size_bytes == 0);
if (dma.current_tag().kind == DmaTag::Kind::CALL) {
// sprite renderer didn't run, let's just get out of here.
for (int i = 0; i < 4; i++) {
dma.read_and_advance();
}
ASSERT(dma.current_tag_offset() == render_state->next_bucket);
return;
}
// Before anything else, some directrenderer DMA might have been sent
{
auto child = prof.make_scoped_child("direct");
if (render_direct(dma, render_state, child)) {
return;
}
}
// First is the distorter
{
auto child = prof.make_scoped_child("distorter");
render_distorter(dma, render_state, child);
}
// next, sprite frame setup.
handle_sprite_frame_setup(dma, render_state->version, render_state, prof);
// 3d sprites
render_3d(dma);
// 2d draw
// m_sprite_renderer.reset_state();
{
auto child = prof.make_scoped_child("2d-group0");
render_2d_group0(dma, render_state, child);
flush_sprites(render_state, prof, false);
}
// shadow draw
render_fake_shadow(dma);
// 2d draw (HUD)
{
auto child = prof.make_scoped_child("2d-group1");
render_2d_group1(dma, render_state, child);
flush_sprites(render_state, prof, true);
}
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glBlendEquation(GL_FUNC_ADD);
// TODO finish this up.
// fmt::print("next bucket is 0x{}\n", render_state->next_bucket);
while (dma.current_tag_offset() != render_state->next_bucket) {
// auto tag = dma.current_tag();
// fmt::print("@ 0x{:x} tag: {}", dma.current_tag_offset(), tag.print());
auto data = dma.read_and_advance();
VifCode code(data.vif0());
// fmt::print(" vif0: {}\n", code.print());
if (code.kind == VifCode::Kind::NOP) {
// fmt::print(" vif1: {}\n", VifCode(data.vif1()).print());
}
}
}
void Sprite3::draw_debug_window() {
ImGui::Checkbox("Glow", &m_enable_glow);
ImGui::Checkbox("new glow", &m_glow_renderer.new_mode);
ImGui::Separator();
ImGui::Text("Distort sprites: %d", m_distort_stats.total_sprites);
ImGui::Text("2D Group 0 (World) blocks: %d sprites: %d", m_debug_stats.blocks_2d_grp0,
m_debug_stats.count_2d_grp0);
ImGui::Text("2D Group 1 (HUD) blocks: %d sprites: %d", m_debug_stats.blocks_2d_grp1,
m_debug_stats.count_2d_grp1);
ImGui::Checkbox("Culling", &m_enable_culling);
ImGui::Checkbox("2d", &m_2d_enable);
ImGui::SameLine();
ImGui::Checkbox("3d", &m_3d_enable);
ImGui::Checkbox("Distort", &m_distort_enable);
ImGui::Checkbox("Distort instancing", &m_enable_distort_instancing);
ImGui::Separator();
m_glow_renderer.draw_debug_window();
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// Render (for real)
void Sprite3::flush_sprites(SharedRenderState* render_state,
ScopedProfilerNode& prof,
bool double_draw) {
glBindVertexArray(m_ogl.vao);
glEnable(GL_PRIMITIVE_RESTART);
glPrimitiveRestartIndex(UINT32_MAX);
// upload vertex buffer
glBindBuffer(GL_ARRAY_BUFFER, m_ogl.vertex_buffer);
glBufferData(GL_ARRAY_BUFFER, m_sprite_idx * sizeof(SpriteVertex3D) * 4, m_vertices_3d.data(),
GL_STREAM_DRAW);
// two passes through the buckets. first to build the index buffer
u32 idx_offset = 0;
for (const auto bucket : m_bucket_list) {
memcpy(&m_index_buffer_data[idx_offset], bucket->ids.data(), bucket->ids.size() * sizeof(u32));
bucket->offset_in_idx_buffer = idx_offset;
idx_offset += bucket->ids.size();
}
// now upload it
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ogl.index_buffer);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, idx_offset * sizeof(u32), m_index_buffer_data.data(),
GL_STREAM_DRAW);
// now do draws!
for (const auto bucket : m_bucket_list) {
u32 tbp = bucket->key >> 32;
DrawMode mode;
mode.as_int() = bucket->key & 0xffffffff;
std::optional<u64> tex;
tex = render_state->texture_pool->lookup(tbp);
if (!tex) {
lg::warn("Failed to find texture at {}, using random (sprite)", tbp);
tex = render_state->texture_pool->get_placeholder_texture();
}
ASSERT(tex);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, *tex);
auto settings = setup_opengl_from_draw_mode(mode, GL_TEXTURE0, false);
glUniform1f(glGetUniformLocation(render_state->shaders[ShaderId::SPRITE3].id(), "alpha_min"),
double_draw ? settings.aref_first : 0.016);
glUniform1f(glGetUniformLocation(render_state->shaders[ShaderId::SPRITE3].id(), "alpha_max"),
10.f);
glUniform1i(glGetUniformLocation(render_state->shaders[ShaderId::SPRITE3].id(), "tex_T0"), 0);
prof.add_draw_call();
prof.add_tri(2 * (bucket->ids.size() / 5));
glDrawElements(GL_TRIANGLE_STRIP, bucket->ids.size(), GL_UNSIGNED_INT,
(void*)(bucket->offset_in_idx_buffer * sizeof(u32)));
if (double_draw) {
switch (settings.kind) {
case DoubleDrawKind::NONE:
break;
case DoubleDrawKind::AFAIL_NO_DEPTH_WRITE:
prof.add_draw_call();
prof.add_tri(2 * (bucket->ids.size() / 5));
glUniform1f(
glGetUniformLocation(render_state->shaders[ShaderId::SPRITE3].id(), "alpha_min"),
-10.f);
glUniform1f(
glGetUniformLocation(render_state->shaders[ShaderId::SPRITE3].id(), "alpha_max"),
settings.aref_second);
glDepthMask(GL_FALSE);
glDrawElements(GL_TRIANGLE_STRIP, bucket->ids.size(), GL_UNSIGNED_INT,
(void*)(bucket->offset_in_idx_buffer * sizeof(u32)));
break;
default:
ASSERT(false);
}
}
}
m_sprite_buckets.clear();
m_bucket_list.clear();
m_last_bucket_key = UINT64_MAX;
m_last_bucket = nullptr;
m_sprite_idx = 0;
glBindVertexArray(0);
}
void Sprite3::handle_tex0(u64 val,
SharedRenderState* /*render_state*/,
ScopedProfilerNode& /*prof*/) {
GsTex0 reg(val);
// update tbp
m_current_tbp = reg.tbp0();
m_current_mode.set_tcc(reg.tcc());
// tbw: assume they got it right
// psm: assume they got it right
// tw: assume they got it right
// th: assume they got it right
ASSERT(reg.tfx() == GsTex0::TextureFunction::MODULATE);
ASSERT(reg.psm() != GsTex0::PSM::PSMT4HH);
// cbp: assume they got it right
// cpsm: assume they got it right
// csm: assume they got it right
}
void Sprite3::handle_tex1(u64 val,
SharedRenderState* /*render_state*/,
ScopedProfilerNode& /*prof*/) {
GsTex1 reg(val);
m_current_mode.set_filt_enable(reg.mmag());
}
void Sprite3::handle_zbuf(u64 val,
SharedRenderState* /*render_state*/,
ScopedProfilerNode& /*prof*/) {
// note: we can basically ignore this. There's a single z buffer that's always configured the same
// way - 24-bit, at offset 448.
GsZbuf x(val);
ASSERT(x.psm() == TextureFormat::PSMZ24);
ASSERT(x.zbp() == 448 || x.zbp() == 304); // 304 for jak 2.
m_current_mode.set_depth_write_enable(!x.zmsk());
}
void Sprite3::handle_clamp(u64 val,
SharedRenderState* /*render_state*/,
ScopedProfilerNode& /*prof*/) {
if (!(val == 0b101 || val == 0 || val == 1 || val == 0b100)) {
ASSERT_MSG(false, fmt::format("clamp: 0x{:x}", val));
}
m_current_mode.set_clamp_s_enable(val & 0b001);
m_current_mode.set_clamp_t_enable(val & 0b100);
}
void Sprite3::update_mode_from_alpha1(u64 val, DrawMode& mode) {
GsAlpha reg(val);
if (reg.a_mode() == GsAlpha::BlendMode::SOURCE && reg.b_mode() == GsAlpha::BlendMode::DEST &&
reg.c_mode() == GsAlpha::BlendMode::SOURCE && reg.d_mode() == GsAlpha::BlendMode::DEST) {
// (Cs - Cd) * As + Cd
// Cs * As + (1 - As) * Cd
mode.set_alpha_blend(DrawMode::AlphaBlend::SRC_DST_SRC_DST);
} else if (reg.a_mode() == GsAlpha::BlendMode::SOURCE &&
reg.b_mode() == GsAlpha::BlendMode::ZERO_OR_FIXED &&
reg.c_mode() == GsAlpha::BlendMode::SOURCE &&
reg.d_mode() == GsAlpha::BlendMode::DEST) {
// (Cs - 0) * As + Cd
// Cs * As + (1) * CD
mode.set_alpha_blend(DrawMode::AlphaBlend::SRC_0_SRC_DST);
} else if (reg.a_mode() == GsAlpha::BlendMode::SOURCE &&
reg.b_mode() == GsAlpha::BlendMode::ZERO_OR_FIXED &&
reg.c_mode() == GsAlpha::BlendMode::ZERO_OR_FIXED &&
reg.d_mode() == GsAlpha::BlendMode::DEST) {
ASSERT(reg.fix() == 128);
// Cv = (Cs - 0) * FIX + Cd
// if fix = 128, it works out to 1.0
mode.set_alpha_blend(DrawMode::AlphaBlend::SRC_0_FIX_DST);
// src plus dest
} else if (reg.a_mode() == GsAlpha::BlendMode::SOURCE &&
reg.b_mode() == GsAlpha::BlendMode::DEST &&
reg.c_mode() == GsAlpha::BlendMode::ZERO_OR_FIXED &&
reg.d_mode() == GsAlpha::BlendMode::DEST) {
// Cv = (Cs - Cd) * FIX + Cd
ASSERT(reg.fix() == 64);
mode.set_alpha_blend(DrawMode::AlphaBlend::SRC_DST_FIX_DST);
} else if (reg.a_mode() == GsAlpha::BlendMode::ZERO_OR_FIXED &&
reg.b_mode() == GsAlpha::BlendMode::SOURCE &&
reg.c_mode() == GsAlpha::BlendMode::SOURCE &&
reg.d_mode() == GsAlpha::BlendMode::DEST) {
// (0 - Cs) * As + Cd
// Cd - Cs * As
// s, d
mode.set_alpha_blend(DrawMode::AlphaBlend::ZERO_SRC_SRC_DST);
}
else {
lg::error("unsupported blend: a {} b {} c {} d {}", (int)reg.a_mode(), (int)reg.b_mode(),
(int)reg.c_mode(), (int)reg.d_mode());
mode.set_alpha_blend(DrawMode::AlphaBlend::SRC_DST_SRC_DST);
ASSERT(false);
}
}
void Sprite3::handle_alpha(u64 val,
SharedRenderState* /*render_state*/,
ScopedProfilerNode& /*prof*/) {
update_mode_from_alpha1(val, m_current_mode);
}
void Sprite3::do_block_common(SpriteMode mode,
u32 count,
SharedRenderState* render_state,
ScopedProfilerNode& prof) {
m_current_mode = m_default_mode;
for (u32 sprite_idx = 0; sprite_idx < count; sprite_idx++) {
if (m_sprite_idx == SPRITE_RENDERER_MAX_SPRITES) {
flush_sprites(render_state, prof, mode == ModeHUD);
}
if (mode == Mode2D && render_state->has_pc_data && m_enable_culling) {
// we can skip sprites that are out of view
// it's probably possible to do this for 3D as well.
auto bsphere = m_vec_data_2d[sprite_idx].xyz_sx;
bsphere.w() = std::max(bsphere.w(), m_vec_data_2d[sprite_idx].sy());
if (bsphere.w() == 0 || !sphere_in_view_ref(bsphere, render_state->camera_planes)) {
continue;
}
}
if (render_state->version > GameVersion::Jak1) {
// glow code sets the matrix to -1,
// jak 2 adds:
// ibltz vi08, L4
// which is set from ilw.y vi08, 1(vi02)
if (m_vec_data_2d[sprite_idx].matrix() == -1) {
continue;
}
}
auto& adgif = m_adgif[sprite_idx];
handle_tex0(adgif.tex0_data, render_state, prof);
handle_tex1(adgif.tex1_data, render_state, prof);
if (GsRegisterAddress(adgif.clamp_addr) == GsRegisterAddress::ZBUF_1) {
handle_zbuf(adgif.clamp_data, render_state, prof);
} else {
handle_clamp(adgif.clamp_data, render_state, prof);
}
handle_alpha(adgif.alpha_data, render_state, prof);
u64 key = (((u64)m_current_tbp) << 32) | m_current_mode.as_int();
Bucket* bucket;
if (key == m_last_bucket_key) {
bucket = m_last_bucket;
} else {
auto it = m_sprite_buckets.find(key);
if (it == m_sprite_buckets.end()) {
bucket = &m_sprite_buckets[key];
bucket->key = key;
m_bucket_list.push_back(bucket);
} else {
bucket = &it->second;
}
}
u32 start_vtx_id = m_sprite_idx * 4;
bucket->ids.push_back(start_vtx_id);
bucket->ids.push_back(start_vtx_id + 1);
bucket->ids.push_back(start_vtx_id + 2);
bucket->ids.push_back(start_vtx_id + 3);
bucket->ids.push_back(UINT32_MAX);
auto& vert1 = m_vertices_3d.at(start_vtx_id + 0);
if (render_state->version == GameVersion::Jak3 || render_state->version == GameVersion::JakX) {
auto flag = m_vec_data_2d[sprite_idx].flag();
if ((flag & 0x10) || (flag & 0x20)) {
// these flags mean we need to swap vertex order around - not yet implemented since it's too
// hard to get right without this code running.
// ASSERT_NOT_REACHED();
}
}
vert1.xyz_sx = m_vec_data_2d[sprite_idx].xyz_sx;
vert1.quat_sy = m_vec_data_2d[sprite_idx].flag_rot_sy;
// ftoi'd in the original game, and I believe the VIF would discard the upper bits on pack
vert1.rgba = m_vec_data_2d[sprite_idx].rgba;
vert1.rgba.x() = (int)vert1.rgba.x() & 0xff;
vert1.rgba.y() = (int)vert1.rgba.y() & 0xff;
vert1.rgba.z() = (int)vert1.rgba.z() & 0xff;
vert1.rgba.w() = (int)vert1.rgba.w() & 0xff;
vert1.rgba /= 255;
vert1.flags_matrix[0] = m_vec_data_2d[sprite_idx].flag();
vert1.flags_matrix[1] = m_vec_data_2d[sprite_idx].matrix();
vert1.info[0] = 0; // hack
vert1.info[1] = m_current_mode.get_tcc_enable();
vert1.info[2] = 0;
vert1.info[3] = mode;
m_vertices_3d.at(start_vtx_id + 1) = vert1;
m_vertices_3d.at(start_vtx_id + 2) = vert1;
m_vertices_3d.at(start_vtx_id + 3) = vert1;
m_vertices_3d.at(start_vtx_id + 1).info[2] = 1;
m_vertices_3d.at(start_vtx_id + 2).info[2] = 3;
m_vertices_3d.at(start_vtx_id + 3).info[2] = 2;
// note that PC swaps the last two vertices
if (render_state->version == GameVersion::Jak3) {
auto flag = m_vec_data_2d[sprite_idx].flag();
switch (flag & 0x30) {
case 0x10:
// FLAG 16: 1, 0, 3, 2
m_vertices_3d.at(start_vtx_id + 0).info[2] = 0;
m_vertices_3d.at(start_vtx_id + 1).info[2] = 1;
m_vertices_3d.at(start_vtx_id + 2).info[2] = 3;
m_vertices_3d.at(start_vtx_id + 3).info[2] = 2;
break;
case 0x20:
// FLAG 32: 3, 2, 1, 0
m_vertices_3d.at(start_vtx_id + 0).info[2] = 3;
m_vertices_3d.at(start_vtx_id + 1).info[2] = 2;
m_vertices_3d.at(start_vtx_id + 2).info[2] = 0;
m_vertices_3d.at(start_vtx_id + 3).info[2] = 1;
break;
case 0x30:
// 2, 3, 0, 1
m_vertices_3d.at(start_vtx_id + 0).info[2] = 2;
m_vertices_3d.at(start_vtx_id + 1).info[2] = 3;
m_vertices_3d.at(start_vtx_id + 2).info[2] = 1;
m_vertices_3d.at(start_vtx_id + 3).info[2] = 0;
break;
}
}
++m_sprite_idx;
}
}