Generate a series of timestamp ranges, join with your data, calculate the overlap and aggregate:
SELECT user_id,
bucket,
coalesce(sum(upper(inters) - lower(inters)), 0) AS duration
FROM (SELECT user_id,
lower(ranges.r) AS bucket,
tsrange(tab.start, tab.end, '[)') * ranges.r AS inters
FROM (SELECT tsrange(st, st + '1 hour'::interval, '[)') AS r
FROM generate_series(
'2021-01-01 00:00:00'::timestamp,
'2021-12-31 23:00:00'::timestamp,
'1 hour'::interval
) AS g(st)
) AS ranges
LEFT JOIN tab
ON tsrange(tab.start, tab.end, '[)') * ranges.r <> 'empty'
) AS intersections
GROUP BY tab.user_id, bucket;