In this context, the 'Kernel' is the set of samples that are used in the 'Screen Space' domain. The name Kernel comes from image kernel (i.e convolution). The kernel contains the sample data that is ultimately transformed into screen space and used to sample against other depth values within an arbitary proximity of the pixel/fragment (the sampling radius).
I fail to understand how they work together in regards of the
sampling, since in the tutorial there are only 4x4 different random
vector rotations, why is the kernel size 64?
While the result depends on both the samples (in the kernel) and the 'noise' (a single random rotation) applied during TBN transform to ALL the samples (in tangent/view space) at that particular fragment, they do not depend on each other in the first place so do not need to be the same size/count.
You could have 32 samples, and 144 'randoms' in your noise if you wanted, it does not matter. The number of samples in the kernel are what is sampled for each fragment. In this example you gave, each fragment will perform 64 samples, and will be TBN transformed with a 'single' noise/random jitter at that particular fragment position. The noise size is picked to simply prevent adjacent repitition (banding) and give the impression of a larger distribution of sample randomness wrt to adjacent fragments (although it is infact repeated).
The values 64 and 16 (4x4) are probably chosen for in this example as they are probably the happy medium for achieving a decent, natural looking, but fast result in this scenario. The purpose of SSOA is a cheap, no-CPU approximation of ambient occlusion (shading from local occlusion), not done in world space, so there needs to be a balance of realistic looking quality, while remaining easy enough for the GPU to retain a decent FPS.
If you display the noise texture alone (using the scaling values when querying the texel in the tutorial) i.e
const vec2 noiseScale = vec2(800.0/4.0, 600.0/4.0); // screen = 800x600, N.B '4' x '4' matrix
frag = texture(texNoise, TexCoords * noiseScale).rgb;
you would see that it is tiled over the entirety of the screen, with each noise value occupying a single fragment/pixel, but repeated over the screen.
e.g. for a 4x4 noise, the value (pixel colour) is repeated every 4 horizontal pixels, and every 4 vertical pixels.
This would be the single 'noise' value used when transforming all 64 samples at that particular fragment location.
The tutorial mentions that it wants to give samples closer to the
current fragment more weight, wouldnt it then make more sense to store
the weight directly in the vector rotations (in the A/W component)?
This wouldn't make any sense since the samples are what are weighted, not the random noise, and since each fragment uses a single noise value it would be nothing more than a scalar applied to ALL samples done at that fragment location, which defeats the purpose of 'weighting the samples'.
Also note, that any sample in the hemisphere is the same distance irrelevant of any rotation applied, and so this 'weighting' is in fact constant (since the samples are constant in the firstplace) so do not need evaluating each time they are used in the shader (waste of GPU cycles).
The sample weighting is done by CPU when populating the buffer (
lerp in the tutorial code) by pushing the 'uniformly' distributed samples closer to the hemisphere souce/origin. i.e more samples closer to the fragment position (when transformed) as these are likely to add more 'effect' to close proximity occlusions. This is why the shading falls off quite fast, and is a lot darker in compacted corners.