No! This “allocate one for each access type, ignoring its underlying heap” is exactly what you don’t do.
You cannot ignore heaps. They are, after all, where the memory ultimately comes from. You cannot allocate more memory than exists. If you allocate 512MB from two different memory types that use the same heap, you have allocated 1GB of memory from that heap.
You’re thinking way too mechanically here. Vulkan is not the kind of API where you can mindlessly just do stuff, particularly as it concerns memory allocations. Your allocation pattern needs to be based on two things: what your application needs to do its job, and what the implementation provides.
Your application needs, for example, images of some particular formats. And your application wants to put those images in the best possible memory. However, that “best possible memory” may not be host accessible. So you have to be able to deal with a number of possibilities:
1: Host-visible, device-local memory: You still need staging, since you need to copy to the optimal image format, so you’ll need additional memory, but it will be of this memory type. So you only need a single allocation.
2: Device-local, but not host-visible: You now need a separate staging memory allocation. And you’ll need to decide whether you want that allocation to be non-coherent or coherent, cached or non-cached, as is appropriate for what the implementation provides and your use cases.
And that assumes that the desired memory (device-local) will work for your particular image formats. Which is something you have to query. If it doesn’t work, then you have to find the best memory which will work.
You cannot have a reasonable allocation strategy without first deciding what your needs are.